1
+ import functools
2
+ import json
1
3
import time
2
4
try : # Python 2
3
5
from urlparse import urljoin
19
21
20
22
21
23
# The __init__.py will import this. Not the other way around.
22
- __version__ = "1.2 .0"
24
+ __version__ = "1.3 .0"
23
25
24
26
logger = logging .getLogger (__name__ )
25
27
@@ -54,11 +56,11 @@ def decorate_scope(
54
56
CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
55
57
56
58
def _get_new_correlation_id ():
57
- return str (uuid .uuid4 ())
59
+ return str (uuid .uuid4 ())
58
60
59
61
60
62
def _build_current_telemetry_request_header (public_api_id , force_refresh = False ):
61
- return "1|{},{}|" .format (public_api_id , "1" if force_refresh else "0" )
63
+ return "1|{},{}|" .format (public_api_id , "1" if force_refresh else "0" )
62
64
63
65
64
66
def extract_certs (public_cert_content ):
@@ -92,12 +94,14 @@ def __init__(
92
94
self , client_id ,
93
95
client_credential = None , authority = None , validate_authority = True ,
94
96
token_cache = None ,
97
+ http_client = None ,
95
98
verify = True , proxies = None , timeout = None ,
96
99
client_claims = None , app_name = None , app_version = None ):
97
100
"""Create an instance of application.
98
101
99
- :param client_id: Your app has a client_id after you register it on AAD.
100
- :param client_credential:
102
+ :param str client_id: Your app has a client_id after you register it on AAD.
103
+
104
+ :param str client_credential:
101
105
For :class:`PublicClientApplication`, you simply use `None` here.
102
106
For :class:`ConfidentialClientApplication`,
103
107
it can be a string containing client secret,
@@ -114,6 +118,17 @@ def __init__(
114
118
which will be sent through 'x5c' JWT header only for
115
119
subject name and issuer authentication to support cert auto rolls.
116
120
121
+ Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_,
122
+ "the certificate containing
123
+ the public key corresponding to the key used to digitally sign the
124
+ JWS MUST be the first certificate. This MAY be followed by
125
+ additional certificates, with each subsequent certificate being the
126
+ one used to certify the previous one."
127
+ However, your certificate's issuer may use a different order.
128
+ So, if your attempt ends up with an error AADSTS700027 -
129
+ "The provided signature value did not match the expected signature value",
130
+ you may try use only the leaf cert (in PEM/str format) instead.
131
+
117
132
:param dict client_claims:
118
133
*Added in version 0.5.0*:
119
134
It is a dictionary of extra claims that would be signed by
@@ -139,18 +154,24 @@ def __init__(
139
154
:param TokenCache cache:
140
155
Sets the token cache used by this ClientApplication instance.
141
156
By default, an in-memory cache will be created and used.
157
+ :param http_client: (optional)
158
+ Your implementation of abstract class HttpClient <msal.oauth2cli.http.http_client>
159
+ Defaults to a requests session instance
142
160
:param verify: (optional)
143
161
It will be passed to the
144
162
`verify parameter in the underlying requests library
145
163
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_
164
+ This does not apply if you have chosen to pass your own Http client
146
165
:param proxies: (optional)
147
166
It will be passed to the
148
167
`proxies parameter in the underlying requests library
149
168
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_
169
+ This does not apply if you have chosen to pass your own Http client
150
170
:param timeout: (optional)
151
171
It will be passed to the
152
172
`timeout parameter in the underlying requests library
153
173
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_
174
+ This does not apply if you have chosen to pass your own Http client
154
175
:param app_name: (optional)
155
176
You can provide your application name for Microsoft telemetry purposes.
156
177
Default value is None, means it will not be passed to Microsoft.
@@ -161,14 +182,21 @@ def __init__(
161
182
self .client_id = client_id
162
183
self .client_credential = client_credential
163
184
self .client_claims = client_claims
164
- self .verify = verify
165
- self .proxies = proxies
166
- self .timeout = timeout
185
+ if http_client :
186
+ self .http_client = http_client
187
+ else :
188
+ self .http_client = requests .Session ()
189
+ self .http_client .verify = verify
190
+ self .http_client .proxies = proxies
191
+ # Requests, does not support session - wide timeout
192
+ # But you can patch that (https://github.com/psf/requests/issues/3341):
193
+ self .http_client .request = functools .partial (
194
+ self .http_client .request , timeout = timeout )
167
195
self .app_name = app_name
168
196
self .app_version = app_version
169
197
self .authority = Authority (
170
198
authority or "https://login.microsoftonline.com/common/" ,
171
- validate_authority , verify = verify , proxies = proxies , timeout = timeout )
199
+ self . http_client , validate_authority = validate_authority )
172
200
# Here the self.authority is not the same type as authority in input
173
201
self .token_cache = token_cache or TokenCache ()
174
202
self .client = self ._build_client (client_credential , self .authority )
@@ -211,14 +239,14 @@ def _build_client(self, client_credential, authority):
211
239
return Client (
212
240
server_configuration ,
213
241
self .client_id ,
242
+ http_client = self .http_client ,
214
243
default_headers = default_headers ,
215
244
default_body = default_body ,
216
245
client_assertion = client_assertion ,
217
246
client_assertion_type = client_assertion_type ,
218
247
on_obtaining_tokens = self .token_cache .add ,
219
248
on_removing_rt = self .token_cache .remove_rt ,
220
- on_updating_rt = self .token_cache .update_rt ,
221
- verify = self .verify , proxies = self .proxies , timeout = self .timeout )
249
+ on_updating_rt = self .token_cache .update_rt )
222
250
223
251
def get_authorization_request_url (
224
252
self ,
@@ -230,6 +258,7 @@ def get_authorization_request_url(
230
258
response_type = "code" , # Can be "token" if you use Implicit Grant
231
259
prompt = None ,
232
260
nonce = None ,
261
+ domain_hint = None , # type: Optional[str]
233
262
** kwargs ):
234
263
"""Constructs a URL for you to start a Authorization Code Grant.
235
264
@@ -251,6 +280,12 @@ def get_authorization_request_url(
251
280
:param nonce:
252
281
A cryptographically random value used to mitigate replay attacks. See also
253
282
`OIDC specs <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
283
+ :param domain_hint:
284
+ Can be one of "consumers" or "organizations" or your tenant domain "contoso.com".
285
+ If included, it will skip the email-based discovery process that user goes
286
+ through on the sign-in page, leading to a slightly more streamlined user experience.
287
+ https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code
288
+ https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8
254
289
:return: The authorization url as a string.
255
290
"""
256
291
""" # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -269,18 +304,20 @@ def get_authorization_request_url(
269
304
# Multi-tenant app can use new authority on demand
270
305
the_authority = Authority (
271
306
authority ,
272
- verify = self .verify , proxies = self . proxies , timeout = self . timeout ,
307
+ self .http_client
273
308
) if authority else self .authority
274
309
275
310
client = Client (
276
311
{"authorization_endpoint" : the_authority .authorization_endpoint },
277
- self .client_id )
312
+ self .client_id ,
313
+ http_client = self .http_client )
278
314
return client .build_auth_request_uri (
279
315
response_type = response_type ,
280
316
redirect_uri = redirect_uri , state = state , login_hint = login_hint ,
281
317
prompt = prompt ,
282
318
scope = decorate_scope (scopes , self .client_id ),
283
319
nonce = nonce ,
320
+ domain_hint = domain_hint ,
284
321
)
285
322
286
323
def acquire_token_by_authorization_code (
@@ -379,13 +416,12 @@ def _find_msal_accounts(self, environment):
379
416
380
417
def _get_authority_aliases (self , instance ):
381
418
if not self .authority_groups :
382
- resp = requests .get (
419
+ resp = self . http_client .get (
383
420
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize" ,
384
- headers = {'Accept' : 'application/json' },
385
- verify = self .verify , proxies = self .proxies , timeout = self .timeout )
421
+ headers = {'Accept' : 'application/json' })
386
422
resp .raise_for_status ()
387
423
self .authority_groups = [
388
- set (group ['aliases' ]) for group in resp . json ( )['metadata' ]]
424
+ set (group ['aliases' ]) for group in json . loads ( resp . text )['metadata' ]]
389
425
for group in self .authority_groups :
390
426
if instance in group :
391
427
return [alias for alias in group if alias != instance ]
@@ -504,7 +540,7 @@ def acquire_token_silent_with_error(
504
540
warnings .warn ("We haven't decided how/if this method will accept authority parameter" )
505
541
# the_authority = Authority(
506
542
# authority,
507
- # verify= self.verify, proxies=self.proxies, timeout=self.timeout ,
543
+ # self.http_client ,
508
544
# ) if authority else self.authority
509
545
result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
510
546
scopes , account , self .authority , force_refresh = force_refresh ,
@@ -516,8 +552,8 @@ def acquire_token_silent_with_error(
516
552
for alias in self ._get_authority_aliases (self .authority .instance ):
517
553
the_authority = Authority (
518
554
"https://" + alias + "/" + self .authority .tenant ,
519
- validate_authority = False ,
520
- verify = self . verify , proxies = self . proxies , timeout = self . timeout )
555
+ self . http_client ,
556
+ validate_authority = False )
521
557
result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
522
558
scopes , account , the_authority , force_refresh = force_refresh ,
523
559
correlation_id = correlation_id ,
@@ -597,16 +633,18 @@ def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
597
633
** kwargs )
598
634
if at and "error" not in at :
599
635
return at
636
+ last_resp = None
600
637
if app_metadata .get ("family_id" ): # Meaning this app belongs to this family
601
- at = self ._acquire_token_silent_by_finding_specific_refresh_token (
638
+ last_resp = at = self ._acquire_token_silent_by_finding_specific_refresh_token (
602
639
authority , scopes , dict (query , family_id = app_metadata ["family_id" ]),
603
640
** kwargs )
604
641
if at and "error" not in at :
605
642
return at
606
643
# Either this app is an orphan, so we will naturally use its own RT;
607
644
# or all attempts above have failed, so we fall back to non-foci behavior.
608
645
return self ._acquire_token_silent_by_finding_specific_refresh_token (
609
- authority , scopes , dict (query , client_id = self .client_id ), ** kwargs )
646
+ authority , scopes , dict (query , client_id = self .client_id ),
647
+ ** kwargs ) or last_resp
610
648
611
649
def _get_app_metadata (self , environment ):
612
650
apps = self .token_cache .find ( # Use find(), rather than token_cache.get(...)
@@ -662,6 +700,36 @@ def _validate_ssh_cert_input_data(self, data):
662
700
"you must include a string parameter named 'key_id' "
663
701
"which identifies the key in the 'req_cnf' argument." )
664
702
703
+ def acquire_token_by_refresh_token (self , refresh_token , scopes ):
704
+ """Acquire token(s) based on a refresh token (RT) obtained from elsewhere.
705
+
706
+ You use this method only when you have old RTs from elsewhere,
707
+ and now you want to migrate them into MSAL.
708
+ Calling this method results in new tokens automatically storing into MSAL.
709
+
710
+ You do NOT need to use this method if you are already using MSAL.
711
+ MSAL maintains RT automatically inside its token cache,
712
+ and an access token can be retrieved
713
+ when you call :func:`~acquire_token_silent`.
714
+
715
+ :param str refresh_token: The old refresh token, as a string.
716
+
717
+ :param list scopes:
718
+ The scopes associate with this old RT.
719
+ Each scope needs to be in the Microsoft identity platform (v2) format.
720
+ See `Scopes not resources <https://docs.microsoft.com/en-us/azure/active-directory/develop/migrate-python-adal-msal#scopes-not-resources>`_.
721
+
722
+ :return:
723
+ * A dict contains "error" and some other keys, when error happened.
724
+ * A dict contains no "error" key means migration was successful.
725
+ """
726
+ return self .client .obtain_token_by_refresh_token (
727
+ refresh_token ,
728
+ decorate_scope (scopes , self .client_id ),
729
+ rt_getter = lambda rt : rt ,
730
+ on_updating_rt = False ,
731
+ )
732
+
665
733
666
734
class PublicClientApplication (ClientApplication ): # browser app or mobile app
667
735
@@ -760,13 +828,11 @@ def acquire_token_by_username_password(
760
828
761
829
def _acquire_token_by_username_password_federated (
762
830
self , user_realm_result , username , password , scopes = None , ** kwargs ):
763
- verify = kwargs .pop ("verify" , self .verify )
764
- proxies = kwargs .pop ("proxies" , self .proxies )
765
831
wstrust_endpoint = {}
766
832
if user_realm_result .get ("federation_metadata_url" ):
767
833
wstrust_endpoint = mex_send_request (
768
834
user_realm_result ["federation_metadata_url" ],
769
- verify = verify , proxies = proxies )
835
+ self . http_client )
770
836
if wstrust_endpoint is None :
771
837
raise ValueError ("Unable to find wstrust endpoint from MEX. "
772
838
"This typically happens when attempting MSA accounts. "
@@ -778,7 +844,7 @@ def _acquire_token_by_username_password_federated(
778
844
wstrust_endpoint .get ("address" ,
779
845
# Fallback to an AAD supplied endpoint
780
846
user_realm_result .get ("federation_active_auth_url" )),
781
- wstrust_endpoint .get ("action" ), verify = verify , proxies = proxies )
847
+ wstrust_endpoint .get ("action" ), self . http_client )
782
848
if not ("token" in wstrust_result and "type" in wstrust_result ):
783
849
raise RuntimeError ("Unsuccessful RSTR. %s" % wstrust_result )
784
850
GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
0 commit comments