2121
2222
2323# The __init__.py will import this. Not the other way around.
24- __version__ = "1.4.3 "
24+ __version__ = "1.5.0 "
2525
2626logger = logging .getLogger (__name__ )
2727
@@ -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 :
@@ -235,6 +253,7 @@ def _build_client(self, client_credential, authority):
235253 "authorization_endpoint" : authority .authorization_endpoint ,
236254 "token_endpoint" : authority .token_endpoint ,
237255 "device_authorization_endpoint" :
256+ authority .device_authorization_endpoint or
238257 urljoin (authority .token_endpoint , "devicecode" ),
239258 }
240259 return Client (
@@ -260,6 +279,7 @@ def get_authorization_request_url(
260279 prompt = None ,
261280 nonce = None ,
262281 domain_hint = None , # type: Optional[str]
282+ claims_challenge = None ,
263283 ** kwargs ):
264284 """Constructs a URL for you to start a Authorization Code Grant.
265285
@@ -288,6 +308,12 @@ def get_authorization_request_url(
288308 More information on possible values
289309 `here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
290310 `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+
291317 :return: The authorization url as a string.
292318 """
293319 """ # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -320,6 +346,8 @@ def get_authorization_request_url(
320346 scope = decorate_scope (scopes , self .client_id ),
321347 nonce = nonce ,
322348 domain_hint = domain_hint ,
349+ claims = _merge_claims_challenge_and_capabilities (
350+ self ._client_capabilities , claims_challenge ),
323351 )
324352
325353 def acquire_token_by_authorization_code (
@@ -331,6 +359,7 @@ def acquire_token_by_authorization_code(
331359 # authorization request as described in Section 4.1.1, and their
332360 # values MUST be identical.
333361 nonce = None ,
362+ claims_challenge = None ,
334363 ** kwargs ):
335364 """The second half of the Authorization Code Grant.
336365
@@ -356,6 +385,12 @@ def acquire_token_by_authorization_code(
356385 same nonce should also be provided here, so that we'll validate it.
357386 An exception will be raised if the nonce in id token mismatches.
358387
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+
359394 :return: A dict representing the json response from AAD:
360395
361396 - A successful response would contain "access_token" key,
@@ -376,6 +411,10 @@ def acquire_token_by_authorization_code(
376411 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
377412 self .ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID ),
378413 },
414+ data = dict (
415+ kwargs .pop ("data" , {}),
416+ claims = _merge_claims_challenge_and_capabilities (
417+ self ._client_capabilities , claims_challenge )),
379418 nonce = nonce ,
380419 ** kwargs )
381420
@@ -478,6 +517,7 @@ def acquire_token_silent(
478517 account , # type: Optional[Account]
479518 authority = None , # See get_authorization_request_url()
480519 force_refresh = False , # type: Optional[boolean]
520+ claims_challenge = None ,
481521 ** kwargs ):
482522 """Acquire an access token for given account, without user interaction.
483523
@@ -492,14 +532,21 @@ def acquire_token_silent(
492532
493533 Internally, this method calls :func:`~acquire_token_silent_with_error`.
494534
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+
495541 :return:
496542 - A dict containing no "error" key,
497543 and typically contains an "access_token" key,
498544 if cache lookup succeeded.
499545 - None when cache lookup does not yield a token.
500546 """
501547 result = self .acquire_token_silent_with_error (
502- scopes , account , authority , force_refresh , ** kwargs )
548+ scopes , account , authority , force_refresh ,
549+ claims_challenge = claims_challenge , ** kwargs )
503550 return result if result and "error" not in result else None
504551
505552 def acquire_token_silent_with_error (
@@ -508,6 +555,7 @@ def acquire_token_silent_with_error(
508555 account , # type: Optional[Account]
509556 authority = None , # See get_authorization_request_url()
510557 force_refresh = False , # type: Optional[boolean]
558+ claims_challenge = None ,
511559 ** kwargs ):
512560 """Acquire an access token for given account, without user interaction.
513561
@@ -528,6 +576,11 @@ def acquire_token_silent_with_error(
528576 :param force_refresh:
529577 If True, it will skip Access Token look-up,
530578 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.
531584 :return:
532585 - A dict containing no "error" key,
533586 and typically contains an "access_token" key,
@@ -546,6 +599,7 @@ def acquire_token_silent_with_error(
546599 # ) if authority else self.authority
547600 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
548601 scopes , account , self .authority , force_refresh = force_refresh ,
602+ claims_challenge = claims_challenge ,
549603 correlation_id = correlation_id ,
550604 ** kwargs )
551605 if result and "error" not in result :
@@ -566,6 +620,7 @@ def acquire_token_silent_with_error(
566620 validate_authority = False )
567621 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
568622 scopes , account , the_authority , force_refresh = force_refresh ,
623+ claims_challenge = claims_challenge ,
569624 correlation_id = correlation_id ,
570625 ** kwargs )
571626 if result :
@@ -588,8 +643,9 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
588643 account , # type: Optional[Account]
589644 authority , # This can be different than self.authority
590645 force_refresh = False , # type: Optional[boolean]
646+ claims_challenge = None ,
591647 ** kwargs ):
592- if not force_refresh :
648+ if not ( force_refresh or claims_challenge ): # Bypass AT when desired or using claims
593649 query = {
594650 "client_id" : self .client_id ,
595651 "environment" : authority .instance ,
@@ -616,7 +672,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
616672 }
617673 return self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
618674 authority , decorate_scope (scopes , self .client_id ), account ,
619- force_refresh = force_refresh , ** kwargs )
675+ force_refresh = force_refresh , claims_challenge = claims_challenge , ** kwargs )
620676
621677 def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
622678 self , authority , scopes , account , ** kwargs ):
@@ -665,7 +721,7 @@ def _get_app_metadata(self, environment):
665721 def _acquire_token_silent_by_finding_specific_refresh_token (
666722 self , authority , scopes , query ,
667723 rt_remover = None , break_condition = lambda response : False ,
668- force_refresh = False , correlation_id = None , ** kwargs ):
724+ force_refresh = False , correlation_id = None , claims_challenge = None , ** kwargs ):
669725 matches = self .token_cache .find (
670726 self .token_cache .CredentialType .REFRESH_TOKEN ,
671727 # target=scopes, # AAD RTs are scope-independent
@@ -685,6 +741,10 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
685741 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
686742 self .ACQUIRE_TOKEN_SILENT_ID , force_refresh = force_refresh ),
687743 },
744+ data = dict (
745+ kwargs .pop ("data" , {}),
746+ claims = _merge_claims_challenge_and_capabilities (
747+ self ._client_capabilities , claims_challenge )),
688748 ** kwargs )
689749 if "error" not in response :
690750 return response
@@ -779,14 +839,19 @@ def initiate_device_flow(self, scopes=None, **kwargs):
779839 flow [self .DEVICE_FLOW_CORRELATION_ID ] = correlation_id
780840 return flow
781841
782- def acquire_token_by_device_flow (self , flow , ** kwargs ):
842+ def acquire_token_by_device_flow (self , flow , claims_challenge = None , ** kwargs ):
783843 """Obtain token by a device flow object, with customizable polling effect.
784844
785845 :param dict flow:
786846 A dict previously generated by :func:`~initiate_device_flow`.
787847 By default, this method's polling effect will block current thread.
788848 You can abort the polling loop at any time,
789849 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.
790855
791856 :return: A dict representing the json response from AAD:
792857
@@ -795,10 +860,14 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
795860 """
796861 return self .client .obtain_token_by_device_flow (
797862 flow ,
798- data = dict (kwargs .pop ("data" , {}), code = flow ["device_code" ]),
799- # 2018-10-4 Hack:
800- # during transition period,
801- # 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+ ),
802871 headers = {
803872 CLIENT_REQUEST_ID :
804873 flow .get (self .DEVICE_FLOW_CORRELATION_ID ) or _get_new_correlation_id (),
@@ -808,7 +877,7 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
808877 ** kwargs )
809878
810879 def acquire_token_by_username_password (
811- self , username , password , scopes , ** kwargs ):
880+ self , username , password , scopes , claims_challenge = None , ** kwargs ):
812881 """Gets a token for a given resource via user credentials.
813882
814883 See this page for constraints of Username Password Flow.
@@ -818,6 +887,11 @@ def acquire_token_by_username_password(
818887 :param str password: The password.
819888 :param list[str] scopes:
820889 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.
821895
822896 :return: A dict representing the json response from AAD:
823897
@@ -830,16 +904,22 @@ def acquire_token_by_username_password(
830904 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
831905 self .ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID ),
832906 }
907+ data = dict (
908+ kwargs .pop ("data" , {}),
909+ claims = _merge_claims_challenge_and_capabilities (
910+ self ._client_capabilities , claims_challenge ))
833911 if not self .authority .is_adfs :
834912 user_realm_result = self .authority .user_realm_discovery (
835913 username , correlation_id = headers [CLIENT_REQUEST_ID ])
836914 if user_realm_result .get ("account_type" ) == "Federated" :
837915 return self ._acquire_token_by_username_password_federated (
838916 user_realm_result , username , password , scopes = scopes ,
917+ data = data ,
839918 headers = headers , ** kwargs )
840919 return self .client .obtain_token_by_username_password (
841920 username , password , scope = scopes ,
842921 headers = headers ,
922+ data = data ,
843923 ** kwargs )
844924
845925 def _acquire_token_by_username_password_federated (
@@ -881,11 +961,16 @@ def _acquire_token_by_username_password_federated(
881961
882962class ConfidentialClientApplication (ClientApplication ): # server-side web app
883963
884- def acquire_token_for_client (self , scopes , ** kwargs ):
964+ def acquire_token_for_client (self , scopes , claims_challenge = None , ** kwargs ):
885965 """Acquires token for the current confidential client, not for an end user.
886966
887967 :param list[str] scopes: (Required)
888968 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.
889974
890975 :return: A dict representing the json response from AAD:
891976
@@ -900,9 +985,13 @@ def acquire_token_for_client(self, scopes, **kwargs):
900985 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
901986 self .ACQUIRE_TOKEN_FOR_CLIENT_ID ),
902987 },
988+ data = dict (
989+ kwargs .pop ("data" , {}),
990+ claims = _merge_claims_challenge_and_capabilities (
991+ self ._client_capabilities , claims_challenge )),
903992 ** kwargs )
904993
905- 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 ):
906995 """Acquires token using on-behalf-of (OBO) flow.
907996
908997 The current app is a middle-tier service which was called with a token
@@ -917,6 +1006,11 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
9171006
9181007 :param str user_assertion: The incoming token already received by this app
9191008 :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.
9201014
9211015 :return: A dict representing the json response from AAD:
9221016
@@ -934,7 +1028,11 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
9341028 # 2. Requesting an IDT (which would otherwise be unavailable)
9351029 # so that the calling app could use id_token_claims to implement
9361030 # their own cache mapping, which is likely needed in web apps.
937- 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 )),
9381036 headers = {
9391037 CLIENT_REQUEST_ID : _get_new_correlation_id (),
9401038 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
0 commit comments