1+ import functools
2+ import json
13import time
24try : # Python 2
35 from urlparse import urljoin
1921
2022
2123# The __init__.py will import this. Not the other way around.
22- __version__ = "1.2 .0"
24+ __version__ = "1.3 .0"
2325
2426logger = logging .getLogger (__name__ )
2527
@@ -54,11 +56,11 @@ def decorate_scope(
5456CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
5557
5658def _get_new_correlation_id ():
57- return str (uuid .uuid4 ())
59+ return str (uuid .uuid4 ())
5860
5961
6062def _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" )
6264
6365
6466def extract_certs (public_cert_content ):
@@ -92,12 +94,14 @@ def __init__(
9294 self , client_id ,
9395 client_credential = None , authority = None , validate_authority = True ,
9496 token_cache = None ,
97+ http_client = None ,
9598 verify = True , proxies = None , timeout = None ,
9699 client_claims = None , app_name = None , app_version = None ):
97100 """Create an instance of application.
98101
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:
101105 For :class:`PublicClientApplication`, you simply use `None` here.
102106 For :class:`ConfidentialClientApplication`,
103107 it can be a string containing client secret,
@@ -114,6 +118,17 @@ def __init__(
114118 which will be sent through 'x5c' JWT header only for
115119 subject name and issuer authentication to support cert auto rolls.
116120
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+
117132 :param dict client_claims:
118133 *Added in version 0.5.0*:
119134 It is a dictionary of extra claims that would be signed by
@@ -139,18 +154,24 @@ def __init__(
139154 :param TokenCache cache:
140155 Sets the token cache used by this ClientApplication instance.
141156 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
142160 :param verify: (optional)
143161 It will be passed to the
144162 `verify parameter in the underlying requests library
145163 <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
146165 :param proxies: (optional)
147166 It will be passed to the
148167 `proxies parameter in the underlying requests library
149168 <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
150170 :param timeout: (optional)
151171 It will be passed to the
152172 `timeout parameter in the underlying requests library
153173 <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
154175 :param app_name: (optional)
155176 You can provide your application name for Microsoft telemetry purposes.
156177 Default value is None, means it will not be passed to Microsoft.
@@ -161,14 +182,21 @@ def __init__(
161182 self .client_id = client_id
162183 self .client_credential = client_credential
163184 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 )
167195 self .app_name = app_name
168196 self .app_version = app_version
169197 self .authority = Authority (
170198 authority or "https://login.microsoftonline.com/common/" ,
171- validate_authority , verify = verify , proxies = proxies , timeout = timeout )
199+ self . http_client , validate_authority = validate_authority )
172200 # Here the self.authority is not the same type as authority in input
173201 self .token_cache = token_cache or TokenCache ()
174202 self .client = self ._build_client (client_credential , self .authority )
@@ -211,14 +239,14 @@ def _build_client(self, client_credential, authority):
211239 return Client (
212240 server_configuration ,
213241 self .client_id ,
242+ http_client = self .http_client ,
214243 default_headers = default_headers ,
215244 default_body = default_body ,
216245 client_assertion = client_assertion ,
217246 client_assertion_type = client_assertion_type ,
218247 on_obtaining_tokens = self .token_cache .add ,
219248 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 )
222250
223251 def get_authorization_request_url (
224252 self ,
@@ -230,6 +258,7 @@ def get_authorization_request_url(
230258 response_type = "code" , # Can be "token" if you use Implicit Grant
231259 prompt = None ,
232260 nonce = None ,
261+ domain_hint = None , # type: Optional[str]
233262 ** kwargs ):
234263 """Constructs a URL for you to start a Authorization Code Grant.
235264
@@ -251,6 +280,12 @@ def get_authorization_request_url(
251280 :param nonce:
252281 A cryptographically random value used to mitigate replay attacks. See also
253282 `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
254289 :return: The authorization url as a string.
255290 """
256291 """ # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -269,18 +304,20 @@ def get_authorization_request_url(
269304 # Multi-tenant app can use new authority on demand
270305 the_authority = Authority (
271306 authority ,
272- verify = self .verify , proxies = self . proxies , timeout = self . timeout ,
307+ self .http_client
273308 ) if authority else self .authority
274309
275310 client = Client (
276311 {"authorization_endpoint" : the_authority .authorization_endpoint },
277- self .client_id )
312+ self .client_id ,
313+ http_client = self .http_client )
278314 return client .build_auth_request_uri (
279315 response_type = response_type ,
280316 redirect_uri = redirect_uri , state = state , login_hint = login_hint ,
281317 prompt = prompt ,
282318 scope = decorate_scope (scopes , self .client_id ),
283319 nonce = nonce ,
320+ domain_hint = domain_hint ,
284321 )
285322
286323 def acquire_token_by_authorization_code (
@@ -379,13 +416,12 @@ def _find_msal_accounts(self, environment):
379416
380417 def _get_authority_aliases (self , instance ):
381418 if not self .authority_groups :
382- resp = requests .get (
419+ resp = self . http_client .get (
383420 "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' })
386422 resp .raise_for_status ()
387423 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' ]]
389425 for group in self .authority_groups :
390426 if instance in group :
391427 return [alias for alias in group if alias != instance ]
@@ -504,7 +540,7 @@ def acquire_token_silent_with_error(
504540 warnings .warn ("We haven't decided how/if this method will accept authority parameter" )
505541 # the_authority = Authority(
506542 # authority,
507- # verify= self.verify, proxies=self.proxies, timeout=self.timeout ,
543+ # self.http_client ,
508544 # ) if authority else self.authority
509545 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
510546 scopes , account , self .authority , force_refresh = force_refresh ,
@@ -516,8 +552,8 @@ def acquire_token_silent_with_error(
516552 for alias in self ._get_authority_aliases (self .authority .instance ):
517553 the_authority = Authority (
518554 "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 )
521557 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
522558 scopes , account , the_authority , force_refresh = force_refresh ,
523559 correlation_id = correlation_id ,
@@ -597,16 +633,18 @@ def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
597633 ** kwargs )
598634 if at and "error" not in at :
599635 return at
636+ last_resp = None
600637 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 (
602639 authority , scopes , dict (query , family_id = app_metadata ["family_id" ]),
603640 ** kwargs )
604641 if at and "error" not in at :
605642 return at
606643 # Either this app is an orphan, so we will naturally use its own RT;
607644 # or all attempts above have failed, so we fall back to non-foci behavior.
608645 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
610648
611649 def _get_app_metadata (self , environment ):
612650 apps = self .token_cache .find ( # Use find(), rather than token_cache.get(...)
@@ -662,6 +700,36 @@ def _validate_ssh_cert_input_data(self, data):
662700 "you must include a string parameter named 'key_id' "
663701 "which identifies the key in the 'req_cnf' argument." )
664702
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+
665733
666734class PublicClientApplication (ClientApplication ): # browser app or mobile app
667735
@@ -760,13 +828,11 @@ def acquire_token_by_username_password(
760828
761829 def _acquire_token_by_username_password_federated (
762830 self , user_realm_result , username , password , scopes = None , ** kwargs ):
763- verify = kwargs .pop ("verify" , self .verify )
764- proxies = kwargs .pop ("proxies" , self .proxies )
765831 wstrust_endpoint = {}
766832 if user_realm_result .get ("federation_metadata_url" ):
767833 wstrust_endpoint = mex_send_request (
768834 user_realm_result ["federation_metadata_url" ],
769- verify = verify , proxies = proxies )
835+ self . http_client )
770836 if wstrust_endpoint is None :
771837 raise ValueError ("Unable to find wstrust endpoint from MEX. "
772838 "This typically happens when attempting MSA accounts. "
@@ -778,7 +844,7 @@ def _acquire_token_by_username_password_federated(
778844 wstrust_endpoint .get ("address" ,
779845 # Fallback to an AAD supplied endpoint
780846 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 )
782848 if not ("token" in wstrust_result and "type" in wstrust_result ):
783849 raise RuntimeError ("Unsuccessful RSTR. %s" % wstrust_result )
784850 GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
0 commit comments