21
21
22
22
23
23
# The __init__.py will import this. Not the other way around.
24
- __version__ = "1.4.3 "
24
+ __version__ = "1.5.0 "
25
25
26
26
logger = logging .getLogger (__name__ )
27
27
@@ -79,6 +79,17 @@ def extract_certs(public_cert_content):
79
79
return [public_cert_content .strip ()]
80
80
81
81
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
+
82
93
class ClientApplication (object ):
83
94
84
95
ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -97,7 +108,8 @@ def __init__(
97
108
token_cache = None ,
98
109
http_client = None ,
99
110
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 ):
101
113
"""Create an instance of application.
102
114
103
115
:param str client_id: Your app has a client_id after you register it on AAD.
@@ -179,10 +191,16 @@ def __init__(
179
191
:param app_version: (optional)
180
192
You can provide your application version for Microsoft telemetry purposes.
181
193
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.
182
199
"""
183
200
self .client_id = client_id
184
201
self .client_credential = client_credential
185
202
self .client_claims = client_claims
203
+ self ._client_capabilities = client_capabilities
186
204
if http_client :
187
205
self .http_client = http_client
188
206
else :
@@ -235,6 +253,7 @@ def _build_client(self, client_credential, authority):
235
253
"authorization_endpoint" : authority .authorization_endpoint ,
236
254
"token_endpoint" : authority .token_endpoint ,
237
255
"device_authorization_endpoint" :
256
+ authority .device_authorization_endpoint or
238
257
urljoin (authority .token_endpoint , "devicecode" ),
239
258
}
240
259
return Client (
@@ -260,6 +279,7 @@ def get_authorization_request_url(
260
279
prompt = None ,
261
280
nonce = None ,
262
281
domain_hint = None , # type: Optional[str]
282
+ claims_challenge = None ,
263
283
** kwargs ):
264
284
"""Constructs a URL for you to start a Authorization Code Grant.
265
285
@@ -288,6 +308,12 @@ def get_authorization_request_url(
288
308
More information on possible values
289
309
`here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
290
310
`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
+
291
317
:return: The authorization url as a string.
292
318
"""
293
319
""" # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -320,6 +346,8 @@ def get_authorization_request_url(
320
346
scope = decorate_scope (scopes , self .client_id ),
321
347
nonce = nonce ,
322
348
domain_hint = domain_hint ,
349
+ claims = _merge_claims_challenge_and_capabilities (
350
+ self ._client_capabilities , claims_challenge ),
323
351
)
324
352
325
353
def acquire_token_by_authorization_code (
@@ -331,6 +359,7 @@ def acquire_token_by_authorization_code(
331
359
# authorization request as described in Section 4.1.1, and their
332
360
# values MUST be identical.
333
361
nonce = None ,
362
+ claims_challenge = None ,
334
363
** kwargs ):
335
364
"""The second half of the Authorization Code Grant.
336
365
@@ -356,6 +385,12 @@ def acquire_token_by_authorization_code(
356
385
same nonce should also be provided here, so that we'll validate it.
357
386
An exception will be raised if the nonce in id token mismatches.
358
387
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
+
359
394
:return: A dict representing the json response from AAD:
360
395
361
396
- A successful response would contain "access_token" key,
@@ -376,6 +411,10 @@ def acquire_token_by_authorization_code(
376
411
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
377
412
self .ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID ),
378
413
},
414
+ data = dict (
415
+ kwargs .pop ("data" , {}),
416
+ claims = _merge_claims_challenge_and_capabilities (
417
+ self ._client_capabilities , claims_challenge )),
379
418
nonce = nonce ,
380
419
** kwargs )
381
420
@@ -478,6 +517,7 @@ def acquire_token_silent(
478
517
account , # type: Optional[Account]
479
518
authority = None , # See get_authorization_request_url()
480
519
force_refresh = False , # type: Optional[boolean]
520
+ claims_challenge = None ,
481
521
** kwargs ):
482
522
"""Acquire an access token for given account, without user interaction.
483
523
@@ -492,14 +532,21 @@ def acquire_token_silent(
492
532
493
533
Internally, this method calls :func:`~acquire_token_silent_with_error`.
494
534
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
+
495
541
:return:
496
542
- A dict containing no "error" key,
497
543
and typically contains an "access_token" key,
498
544
if cache lookup succeeded.
499
545
- None when cache lookup does not yield a token.
500
546
"""
501
547
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 )
503
550
return result if result and "error" not in result else None
504
551
505
552
def acquire_token_silent_with_error (
@@ -508,6 +555,7 @@ def acquire_token_silent_with_error(
508
555
account , # type: Optional[Account]
509
556
authority = None , # See get_authorization_request_url()
510
557
force_refresh = False , # type: Optional[boolean]
558
+ claims_challenge = None ,
511
559
** kwargs ):
512
560
"""Acquire an access token for given account, without user interaction.
513
561
@@ -528,6 +576,11 @@ def acquire_token_silent_with_error(
528
576
:param force_refresh:
529
577
If True, it will skip Access Token look-up,
530
578
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.
531
584
:return:
532
585
- A dict containing no "error" key,
533
586
and typically contains an "access_token" key,
@@ -546,6 +599,7 @@ def acquire_token_silent_with_error(
546
599
# ) if authority else self.authority
547
600
result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
548
601
scopes , account , self .authority , force_refresh = force_refresh ,
602
+ claims_challenge = claims_challenge ,
549
603
correlation_id = correlation_id ,
550
604
** kwargs )
551
605
if result and "error" not in result :
@@ -566,6 +620,7 @@ def acquire_token_silent_with_error(
566
620
validate_authority = False )
567
621
result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
568
622
scopes , account , the_authority , force_refresh = force_refresh ,
623
+ claims_challenge = claims_challenge ,
569
624
correlation_id = correlation_id ,
570
625
** kwargs )
571
626
if result :
@@ -588,8 +643,9 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
588
643
account , # type: Optional[Account]
589
644
authority , # This can be different than self.authority
590
645
force_refresh = False , # type: Optional[boolean]
646
+ claims_challenge = None ,
591
647
** kwargs ):
592
- if not force_refresh :
648
+ if not ( force_refresh or claims_challenge ): # Bypass AT when desired or using claims
593
649
query = {
594
650
"client_id" : self .client_id ,
595
651
"environment" : authority .instance ,
@@ -616,7 +672,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
616
672
}
617
673
return self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
618
674
authority , decorate_scope (scopes , self .client_id ), account ,
619
- force_refresh = force_refresh , ** kwargs )
675
+ force_refresh = force_refresh , claims_challenge = claims_challenge , ** kwargs )
620
676
621
677
def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
622
678
self , authority , scopes , account , ** kwargs ):
@@ -665,7 +721,7 @@ def _get_app_metadata(self, environment):
665
721
def _acquire_token_silent_by_finding_specific_refresh_token (
666
722
self , authority , scopes , query ,
667
723
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 ):
669
725
matches = self .token_cache .find (
670
726
self .token_cache .CredentialType .REFRESH_TOKEN ,
671
727
# target=scopes, # AAD RTs are scope-independent
@@ -685,6 +741,10 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
685
741
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
686
742
self .ACQUIRE_TOKEN_SILENT_ID , force_refresh = force_refresh ),
687
743
},
744
+ data = dict (
745
+ kwargs .pop ("data" , {}),
746
+ claims = _merge_claims_challenge_and_capabilities (
747
+ self ._client_capabilities , claims_challenge )),
688
748
** kwargs )
689
749
if "error" not in response :
690
750
return response
@@ -779,14 +839,19 @@ def initiate_device_flow(self, scopes=None, **kwargs):
779
839
flow [self .DEVICE_FLOW_CORRELATION_ID ] = correlation_id
780
840
return flow
781
841
782
- def acquire_token_by_device_flow (self , flow , ** kwargs ):
842
+ def acquire_token_by_device_flow (self , flow , claims_challenge = None , ** kwargs ):
783
843
"""Obtain token by a device flow object, with customizable polling effect.
784
844
785
845
:param dict flow:
786
846
A dict previously generated by :func:`~initiate_device_flow`.
787
847
By default, this method's polling effect will block current thread.
788
848
You can abort the polling loop at any time,
789
849
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.
790
855
791
856
:return: A dict representing the json response from AAD:
792
857
@@ -795,10 +860,14 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
795
860
"""
796
861
return self .client .obtain_token_by_device_flow (
797
862
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
+ ),
802
871
headers = {
803
872
CLIENT_REQUEST_ID :
804
873
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):
808
877
** kwargs )
809
878
810
879
def acquire_token_by_username_password (
811
- self , username , password , scopes , ** kwargs ):
880
+ self , username , password , scopes , claims_challenge = None , ** kwargs ):
812
881
"""Gets a token for a given resource via user credentials.
813
882
814
883
See this page for constraints of Username Password Flow.
@@ -818,6 +887,11 @@ def acquire_token_by_username_password(
818
887
:param str password: The password.
819
888
:param list[str] scopes:
820
889
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.
821
895
822
896
:return: A dict representing the json response from AAD:
823
897
@@ -830,16 +904,22 @@ def acquire_token_by_username_password(
830
904
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
831
905
self .ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID ),
832
906
}
907
+ data = dict (
908
+ kwargs .pop ("data" , {}),
909
+ claims = _merge_claims_challenge_and_capabilities (
910
+ self ._client_capabilities , claims_challenge ))
833
911
if not self .authority .is_adfs :
834
912
user_realm_result = self .authority .user_realm_discovery (
835
913
username , correlation_id = headers [CLIENT_REQUEST_ID ])
836
914
if user_realm_result .get ("account_type" ) == "Federated" :
837
915
return self ._acquire_token_by_username_password_federated (
838
916
user_realm_result , username , password , scopes = scopes ,
917
+ data = data ,
839
918
headers = headers , ** kwargs )
840
919
return self .client .obtain_token_by_username_password (
841
920
username , password , scope = scopes ,
842
921
headers = headers ,
922
+ data = data ,
843
923
** kwargs )
844
924
845
925
def _acquire_token_by_username_password_federated (
@@ -881,11 +961,16 @@ def _acquire_token_by_username_password_federated(
881
961
882
962
class ConfidentialClientApplication (ClientApplication ): # server-side web app
883
963
884
- def acquire_token_for_client (self , scopes , ** kwargs ):
964
+ def acquire_token_for_client (self , scopes , claims_challenge = None , ** kwargs ):
885
965
"""Acquires token for the current confidential client, not for an end user.
886
966
887
967
:param list[str] scopes: (Required)
888
968
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.
889
974
890
975
:return: A dict representing the json response from AAD:
891
976
@@ -900,9 +985,13 @@ def acquire_token_for_client(self, scopes, **kwargs):
900
985
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
901
986
self .ACQUIRE_TOKEN_FOR_CLIENT_ID ),
902
987
},
988
+ data = dict (
989
+ kwargs .pop ("data" , {}),
990
+ claims = _merge_claims_challenge_and_capabilities (
991
+ self ._client_capabilities , claims_challenge )),
903
992
** kwargs )
904
993
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 ):
906
995
"""Acquires token using on-behalf-of (OBO) flow.
907
996
908
997
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):
917
1006
918
1007
:param str user_assertion: The incoming token already received by this app
919
1008
: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.
920
1014
921
1015
:return: A dict representing the json response from AAD:
922
1016
@@ -934,7 +1028,11 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
934
1028
# 2. Requesting an IDT (which would otherwise be unavailable)
935
1029
# so that the calling app could use id_token_claims to implement
936
1030
# 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 )),
938
1036
headers = {
939
1037
CLIENT_REQUEST_ID : _get_new_correlation_id (),
940
1038
CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
0 commit comments