Skip to content

Commit c235d4e

Browse files
authored
Allowing transport layer to be customized (#169)
1 parent 9229ff3 commit c235d4e

File tree

11 files changed

+200
-116
lines changed

11 files changed

+200
-116
lines changed

msal/application.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
import json
13
import time
24
try: # Python 2
35
from urlparse import urljoin
@@ -54,11 +56,11 @@ def decorate_scope(
5456
CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
5557

5658
def _get_new_correlation_id():
57-
return str(uuid.uuid4())
59+
return str(uuid.uuid4())
5860

5961

6062
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")
6264

6365

6466
def extract_certs(public_cert_content):
@@ -92,6 +94,7 @@ 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.
@@ -151,18 +154,24 @@ def __init__(
151154
:param TokenCache cache:
152155
Sets the token cache used by this ClientApplication instance.
153156
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
154160
:param verify: (optional)
155161
It will be passed to the
156162
`verify parameter in the underlying requests library
157163
<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
158165
:param proxies: (optional)
159166
It will be passed to the
160167
`proxies parameter in the underlying requests library
161168
<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
162170
:param timeout: (optional)
163171
It will be passed to the
164172
`timeout parameter in the underlying requests library
165173
<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
166175
:param app_name: (optional)
167176
You can provide your application name for Microsoft telemetry purposes.
168177
Default value is None, means it will not be passed to Microsoft.
@@ -173,14 +182,21 @@ def __init__(
173182
self.client_id = client_id
174183
self.client_credential = client_credential
175184
self.client_claims = client_claims
176-
self.verify = verify
177-
self.proxies = proxies
178-
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)
179195
self.app_name = app_name
180196
self.app_version = app_version
181197
self.authority = Authority(
182198
authority or "https://login.microsoftonline.com/common/",
183-
validate_authority, verify=verify, proxies=proxies, timeout=timeout)
199+
self.http_client, validate_authority=validate_authority)
184200
# Here the self.authority is not the same type as authority in input
185201
self.token_cache = token_cache or TokenCache()
186202
self.client = self._build_client(client_credential, self.authority)
@@ -223,14 +239,14 @@ def _build_client(self, client_credential, authority):
223239
return Client(
224240
server_configuration,
225241
self.client_id,
242+
http_client=self.http_client,
226243
default_headers=default_headers,
227244
default_body=default_body,
228245
client_assertion=client_assertion,
229246
client_assertion_type=client_assertion_type,
230247
on_obtaining_tokens=self.token_cache.add,
231248
on_removing_rt=self.token_cache.remove_rt,
232-
on_updating_rt=self.token_cache.update_rt,
233-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
249+
on_updating_rt=self.token_cache.update_rt)
234250

235251
def get_authorization_request_url(
236252
self,
@@ -288,12 +304,13 @@ def get_authorization_request_url(
288304
# Multi-tenant app can use new authority on demand
289305
the_authority = Authority(
290306
authority,
291-
verify=self.verify, proxies=self.proxies, timeout=self.timeout,
307+
self.http_client
292308
) if authority else self.authority
293309

294310
client = Client(
295311
{"authorization_endpoint": the_authority.authorization_endpoint},
296-
self.client_id)
312+
self.client_id,
313+
http_client=self.http_client)
297314
return client.build_auth_request_uri(
298315
response_type=response_type,
299316
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
@@ -399,13 +416,12 @@ def _find_msal_accounts(self, environment):
399416

400417
def _get_authority_aliases(self, instance):
401418
if not self.authority_groups:
402-
resp = requests.get(
419+
resp = self.http_client.get(
403420
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize",
404-
headers={'Accept': 'application/json'},
405-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
421+
headers={'Accept': 'application/json'})
406422
resp.raise_for_status()
407423
self.authority_groups = [
408-
set(group['aliases']) for group in resp.json()['metadata']]
424+
set(group['aliases']) for group in json.loads(resp.text)['metadata']]
409425
for group in self.authority_groups:
410426
if instance in group:
411427
return [alias for alias in group if alias != instance]
@@ -524,7 +540,7 @@ def acquire_token_silent_with_error(
524540
warnings.warn("We haven't decided how/if this method will accept authority parameter")
525541
# the_authority = Authority(
526542
# authority,
527-
# verify=self.verify, proxies=self.proxies, timeout=self.timeout,
543+
# self.http_client,
528544
# ) if authority else self.authority
529545
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
530546
scopes, account, self.authority, force_refresh=force_refresh,
@@ -536,8 +552,8 @@ def acquire_token_silent_with_error(
536552
for alias in self._get_authority_aliases(self.authority.instance):
537553
the_authority = Authority(
538554
"https://" + alias + "/" + self.authority.tenant,
539-
validate_authority=False,
540-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
555+
self.http_client,
556+
validate_authority=False)
541557
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
542558
scopes, account, the_authority, force_refresh=force_refresh,
543559
correlation_id=correlation_id,
@@ -780,13 +796,11 @@ def acquire_token_by_username_password(
780796

781797
def _acquire_token_by_username_password_federated(
782798
self, user_realm_result, username, password, scopes=None, **kwargs):
783-
verify = kwargs.pop("verify", self.verify)
784-
proxies = kwargs.pop("proxies", self.proxies)
785799
wstrust_endpoint = {}
786800
if user_realm_result.get("federation_metadata_url"):
787801
wstrust_endpoint = mex_send_request(
788802
user_realm_result["federation_metadata_url"],
789-
verify=verify, proxies=proxies)
803+
self.http_client)
790804
if wstrust_endpoint is None:
791805
raise ValueError("Unable to find wstrust endpoint from MEX. "
792806
"This typically happens when attempting MSA accounts. "
@@ -798,7 +812,7 @@ def _acquire_token_by_username_password_federated(
798812
wstrust_endpoint.get("address",
799813
# Fallback to an AAD supplied endpoint
800814
user_realm_result.get("federation_active_auth_url")),
801-
wstrust_endpoint.get("action"), verify=verify, proxies=proxies)
815+
wstrust_endpoint.get("action"), self.http_client)
802816
if not ("token" in wstrust_result and "type" in wstrust_result):
803817
raise RuntimeError("Unsuccessful RSTR. %s" % wstrust_result)
804818
GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'

msal/authority.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
import json
12
try:
23
from urllib.parse import urlparse
34
except ImportError: # Fall back to Python 2
45
from urlparse import urlparse
56
import logging
67

7-
import requests
8-
98
from .exceptions import MsalServiceError
109

1110

@@ -25,6 +24,7 @@
2524
"b2clogin.de",
2625
]
2726

27+
2828
class Authority(object):
2929
"""This class represents an (already-validated) authority.
3030
@@ -33,9 +33,7 @@ class Authority(object):
3333
"""
3434
_domains_without_user_realm_discovery = set([])
3535

36-
def __init__(self, authority_url, validate_authority=True,
37-
verify=True, proxies=None, timeout=None,
38-
):
36+
def __init__(self, authority_url, http_client, validate_authority=True):
3937
"""Creates an authority instance, and also validates it.
4038
4139
:param validate_authority:
@@ -44,9 +42,7 @@ def __init__(self, authority_url, validate_authority=True,
4442
This parameter only controls whether an instance discovery will be
4543
performed.
4644
"""
47-
self.verify = verify
48-
self.proxies = proxies
49-
self.timeout = timeout
45+
self.http_client = http_client
5046
authority, self.instance, tenant = canonicalize(authority_url)
5147
parts = authority.path.split('/')
5248
is_b2c = any(self.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS) or (
@@ -56,7 +52,7 @@ def __init__(self, authority_url, validate_authority=True,
5652
payload = instance_discovery(
5753
"https://{}{}/oauth2/v2.0/authorize".format(
5854
self.instance, authority.path),
59-
verify=verify, proxies=proxies, timeout=timeout)
55+
self.http_client)
6056
if payload.get("error") == "invalid_instance":
6157
raise ValueError(
6258
"invalid_instance: "
@@ -75,7 +71,7 @@ def __init__(self, authority_url, validate_authority=True,
7571
))
7672
openid_config = tenant_discovery(
7773
tenant_discovery_endpoint,
78-
verify=verify, proxies=proxies, timeout=timeout)
74+
self.http_client)
7975
logger.debug("openid_config = %s", openid_config)
8076
self.authorization_endpoint = openid_config['authorization_endpoint']
8177
self.token_endpoint = openid_config['token_endpoint']
@@ -87,15 +83,14 @@ def user_realm_discovery(self, username, correlation_id=None, response=None):
8783
# "federation_protocol", "cloud_audience_urn",
8884
# "federation_metadata_url", "federation_active_auth_url", etc.
8985
if self.instance not in self.__class__._domains_without_user_realm_discovery:
90-
resp = response or requests.get(
86+
resp = response or self.http_client.get(
9187
"https://{netloc}/common/userrealm/{username}?api-version=1.0".format(
9288
netloc=self.instance, username=username),
93-
headers={'Accept':'application/json',
94-
'client-request-id': correlation_id},
95-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
89+
headers={'Accept': 'application/json',
90+
'client-request-id': correlation_id},)
9691
if resp.status_code != 404:
9792
resp.raise_for_status()
98-
return resp.json()
93+
return json.loads(resp.text)
9994
self.__class__._domains_without_user_realm_discovery.add(self.instance)
10095
return {} # This can guide the caller to fall back normal ROPC flow
10196

@@ -113,20 +108,21 @@ def canonicalize(authority_url):
113108
% authority_url)
114109
return authority, authority.hostname, parts[1]
115110

116-
def instance_discovery(url, **kwargs):
117-
return requests.get( # Note: This URL seemingly returns V1 endpoint only
111+
def instance_discovery(url, http_client, **kwargs):
112+
resp = http_client.get( # Note: This URL seemingly returns V1 endpoint only
118113
'https://{}/common/discovery/instance'.format(
119114
WORLD_WIDE # Historically using WORLD_WIDE. Could use self.instance too
120115
# See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadInstanceDiscovery.cs#L101-L103
121116
# and https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadAuthority.cs#L19-L33
122117
),
123118
params={'authorization_endpoint': url, 'api-version': '1.0'},
124-
**kwargs).json()
119+
**kwargs)
120+
return json.loads(resp.text)
125121

126-
def tenant_discovery(tenant_discovery_endpoint, **kwargs):
122+
def tenant_discovery(tenant_discovery_endpoint, http_client, **kwargs):
127123
# Returns Openid Configuration
128-
resp = requests.get(tenant_discovery_endpoint, **kwargs)
129-
payload = resp.json()
124+
resp = http_client.get(tenant_discovery_endpoint, **kwargs)
125+
payload = json.loads(resp.text)
130126
if 'authorization_endpoint' in payload and 'token_endpoint' in payload:
131127
return payload
132128
raise MsalServiceError(status_code=resp.status_code, **payload)

msal/mex.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,14 @@
3434
except ImportError:
3535
from xml.etree import ElementTree as ET
3636

37-
import requests
38-
3937

4038
def _xpath_of_root(route_to_leaf):
4139
# Construct an xpath suitable to find a root node which has a specified leaf
4240
return '/'.join(route_to_leaf + ['..'] * (len(route_to_leaf)-1))
4341

44-
def send_request(mex_endpoint, **kwargs):
45-
mex_document = requests.get(
42+
43+
def send_request(mex_endpoint, http_client, **kwargs):
44+
mex_document = http_client.get(
4645
mex_endpoint, headers={'Content-Type': 'application/soap+xml'},
4746
**kwargs).text
4847
return Mex(mex_document).get_wstrust_username_password_endpoint()

0 commit comments

Comments
 (0)