Skip to content

Commit 814d710

Browse files
authored
Merge pull request #214 from AzureAD/release-1.4.0
MSAL Python 1.4.0
2 parents 3d24f53 + ec6432b commit 814d710

File tree

9 files changed

+57
-25
lines changed

9 files changed

+57
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ There will be some variations for different flows. They are demonstrated in
5050
from msal import PublicClientApplication
5151
app = PublicClientApplication(
5252
"your_client_id",
53-
"authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here")
53+
authority="https://login.microsoftonline.com/Enter_the_Tenant_Name_Here")
5454
```
5555

5656
Later, each time you would want an access token, you start by:

msal/application.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
# The __init__.py will import this. Not the other way around.
24-
__version__ = "1.3.0"
24+
__version__ = "1.4.0"
2525

2626
logger = logging.getLogger(__name__)
2727

@@ -198,8 +198,9 @@ def __init__(
198198
authority or "https://login.microsoftonline.com/common/",
199199
self.http_client, validate_authority=validate_authority)
200200
# Here the self.authority is not the same type as authority in input
201+
self.client = None
201202
self.token_cache = token_cache or TokenCache()
202-
self.client = self._build_client(client_credential, self.authority)
203+
self._client_credential = client_credential
203204
self.authority_groups = None
204205

205206
def _build_client(self, client_credential, authority):
@@ -248,6 +249,12 @@ def _build_client(self, client_credential, authority):
248249
on_removing_rt=self.token_cache.remove_rt,
249250
on_updating_rt=self.token_cache.update_rt)
250251

252+
def _get_client(self):
253+
if not self.client:
254+
self.authority.initialize()
255+
self.client = self._build_client(self._client_credential, self.authority)
256+
return self.client
257+
251258
def get_authorization_request_url(
252259
self,
253260
scopes, # type: list[str]
@@ -284,8 +291,9 @@ def get_authorization_request_url(
284291
Can be one of "consumers" or "organizations" or your tenant domain "contoso.com".
285292
If included, it will skip the email-based discovery process that user goes
286293
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
294+
More information on possible values
295+
`here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
296+
`here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
289297
:return: The authorization url as a string.
290298
"""
291299
""" # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -306,6 +314,7 @@ def get_authorization_request_url(
306314
authority,
307315
self.http_client
308316
) if authority else self.authority
317+
the_authority.initialize()
309318

310319
client = Client(
311320
{"authorization_endpoint": the_authority.authorization_endpoint},
@@ -366,7 +375,7 @@ def acquire_token_by_authorization_code(
366375
# really empty.
367376
assert isinstance(scopes, list), "Invalid parameter type"
368377
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
369-
return self.client.obtain_token_by_authorization_code(
378+
return self._get_client().obtain_token_by_authorization_code(
370379
code, redirect_uri=redirect_uri,
371380
scope=decorate_scope(scopes, self.client_id),
372381
headers={
@@ -390,6 +399,7 @@ def get_accounts(self, username=None):
390399
Your app can choose to display those information to end user,
391400
and allow user to choose one of his/her accounts to proceed.
392401
"""
402+
self.authority.initialize()
393403
accounts = self._find_msal_accounts(environment=self.authority.instance)
394404
if not accounts: # Now try other aliases of this authority instance
395405
for alias in self._get_authority_aliases(self.authority.instance):
@@ -542,6 +552,7 @@ def acquire_token_silent_with_error(
542552
# authority,
543553
# self.http_client,
544554
# ) if authority else self.authority
555+
self.authority.initialize()
545556
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
546557
scopes, account, self.authority, force_refresh=force_refresh,
547558
correlation_id=correlation_id,
@@ -554,6 +565,7 @@ def acquire_token_silent_with_error(
554565
"https://" + alias + "/" + self.authority.tenant,
555566
self.http_client,
556567
validate_authority=False)
568+
the_authority.initialize()
557569
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
558570
scopes, account, the_authority, force_refresh=force_refresh,
559571
correlation_id=correlation_id,
@@ -723,11 +735,12 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes):
723735
* A dict contains "error" and some other keys, when error happened.
724736
* A dict contains no "error" key means migration was successful.
725737
"""
726-
return self.client.obtain_token_by_refresh_token(
738+
return self._get_client().obtain_token_by_refresh_token(
727739
refresh_token,
728-
decorate_scope(scopes, self.client_id),
740+
scope=decorate_scope(scopes, self.client_id),
729741
rt_getter=lambda rt: rt,
730742
on_updating_rt=False,
743+
on_removing_rt=lambda rt_item: None, # No OP
731744
)
732745

733746

@@ -753,7 +766,7 @@ def initiate_device_flow(self, scopes=None, **kwargs):
753766
- an error response would contain some other readable key/value pairs.
754767
"""
755768
correlation_id = _get_new_correlation_id()
756-
flow = self.client.initiate_device_flow(
769+
flow = self._get_client().initiate_device_flow(
757770
scope=decorate_scope(scopes or [], self.client_id),
758771
headers={
759772
CLIENT_REQUEST_ID: correlation_id,
@@ -777,7 +790,7 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
777790
- A successful response would contain "access_token" key,
778791
- an error response would contain "error" and usually "error_description".
779792
"""
780-
return self.client.obtain_token_by_device_flow(
793+
return self._get_client().obtain_token_by_device_flow(
781794
flow,
782795
data=dict(kwargs.pop("data", {}), code=flow["device_code"]),
783796
# 2018-10-4 Hack:
@@ -814,14 +827,15 @@ def acquire_token_by_username_password(
814827
CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header(
815828
self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID),
816829
}
830+
self.authority.initialize()
817831
if not self.authority.is_adfs:
818832
user_realm_result = self.authority.user_realm_discovery(
819833
username, correlation_id=headers[CLIENT_REQUEST_ID])
820834
if user_realm_result.get("account_type") == "Federated":
821835
return self._acquire_token_by_username_password_federated(
822836
user_realm_result, username, password, scopes=scopes,
823837
headers=headers, **kwargs)
824-
return self.client.obtain_token_by_username_password(
838+
return self._get_client().obtain_token_by_username_password(
825839
username, password, scope=scopes,
826840
headers=headers,
827841
**kwargs)
@@ -850,16 +864,16 @@ def _acquire_token_by_username_password_federated(
850864
GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'
851865
grant_type = {
852866
SAML_TOKEN_TYPE_V1: GRANT_TYPE_SAML1_1,
853-
SAML_TOKEN_TYPE_V2: self.client.GRANT_TYPE_SAML2,
867+
SAML_TOKEN_TYPE_V2: Client.GRANT_TYPE_SAML2,
854868
WSS_SAML_TOKEN_PROFILE_V1_1: GRANT_TYPE_SAML1_1,
855-
WSS_SAML_TOKEN_PROFILE_V2: self.client.GRANT_TYPE_SAML2
869+
WSS_SAML_TOKEN_PROFILE_V2: Client.GRANT_TYPE_SAML2
856870
}.get(wstrust_result.get("type"))
857871
if not grant_type:
858872
raise RuntimeError(
859873
"RSTR returned unknown token type: %s", wstrust_result.get("type"))
860-
self.client.grant_assertion_encoders.setdefault( # Register a non-standard type
861-
grant_type, self.client.encode_saml_assertion)
862-
return self.client.obtain_token_by_assertion(
874+
Client.grant_assertion_encoders.setdefault( # Register a non-standard type
875+
grant_type, Client.encode_saml_assertion)
876+
return self._get_client().obtain_token_by_assertion(
863877
wstrust_result["token"], grant_type, scope=scopes, **kwargs)
864878

865879

@@ -877,7 +891,7 @@ def acquire_token_for_client(self, scopes, **kwargs):
877891
- an error response would contain "error" and usually "error_description".
878892
"""
879893
# TBD: force_refresh behavior
880-
return self.client.obtain_token_for_client(
894+
return self._get_client().obtain_token_for_client(
881895
scope=scopes, # This grant flow requires no scope decoration
882896
headers={
883897
CLIENT_REQUEST_ID: _get_new_correlation_id(),
@@ -909,9 +923,9 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
909923
"""
910924
# The implementation is NOT based on Token Exchange
911925
# https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
912-
return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
926+
return self._get_client().obtain_token_by_assertion( # bases on assertion RFC 7521
913927
user_assertion,
914-
self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs
928+
Client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs
915929
scope=decorate_scope(scopes, self.client_id), # Decoration is used for:
916930
# 1. Explicitly requesting an RT, without relying on AAD default
917931
# behavior, even though it currently still issues an RT.

msal/authority.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def __init__(self, authority_url, http_client, validate_authority=True):
5252
This parameter only controls whether an instance discovery will be
5353
performed.
5454
"""
55+
self._http_client = http_client
56+
self._authority_url = authority_url
57+
self._validate_authority = validate_authority
58+
self._is_initialized = False
59+
60+
def initialize(self):
61+
if not self._is_initialized:
62+
self.__initialize(self._authority_url, self._http_client, self._validate_authority)
63+
self._is_initialized = True
64+
65+
def __initialize(self, authority_url, http_client, validate_authority):
5566
self._http_client = http_client
5667
authority, self.instance, tenant = canonicalize(authority_url)
5768
parts = authority.path.split('/')

msal/oauth2cli/oauth2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def obtain_token_by_refresh_token(self, refresh_token, scope=None, **kwargs):
233233
234234
:param refresh_token: The refresh token issued to the client
235235
:param scope: If omitted, is treated as equal to the scope originally
236-
granted by the resource ownser,
236+
granted by the resource owner,
237237
according to https://tools.ietf.org/html/rfc6749#section-6
238238
"""
239239
assert isinstance(refresh_token, string_types)
@@ -397,7 +397,7 @@ def parse_auth_response(params, state=None):
397397

398398
def obtain_token_by_authorization_code(
399399
self, code, redirect_uri=None, scope=None, **kwargs):
400-
"""Get a token via auhtorization code. a.k.a. Authorization Code Grant.
400+
"""Get a token via authorization code. a.k.a. Authorization Code Grant.
401401
402402
This is typically used by a server-side app (Confidential Client),
403403
but it can also be used by a device-side native app (Public Client).
@@ -503,7 +503,7 @@ def obtain_token_by_refresh_token(self, token_item, scope=None,
503503
Either way, this token_item will be passed into other callbacks as-is.
504504
505505
:param scope: If omitted, is treated as equal to the scope originally
506-
granted by the resource ownser,
506+
granted by the resource owner,
507507
according to https://tools.ietf.org/html/rfc6749#section-6
508508
:param rt_getter: A callable to translate the token_item to a raw RT string
509509
:param on_removing_rt: If absent, fall back to the one defined in initialization

msal/oauth2cli/oidc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def build_auth_request_uri(self, response_type, nonce=None, **kwargs):
9999
response_type, nonce=nonce, **kwargs)
100100

101101
def obtain_token_by_authorization_code(self, code, nonce=None, **kwargs):
102-
"""Get a token via auhtorization code. a.k.a. Authorization Code Grant.
102+
"""Get a token via authorization code. a.k.a. Authorization Code Grant.
103103
104104
Return value and all other parameters are the same as
105105
:func:`oauth2.Client.obtain_token_by_authorization_code`,

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[bdist_wheel]
22
universal=1
33

4+
[metadata]
5+
project_urls =
6+
Changelog = https://github.com/AzureAD/microsoft-authentication-library-for-python/releases

tests/test_application.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def setUp(self):
104104
self.authority_url = "https://login.microsoftonline.com/common"
105105
self.authority = msal.authority.Authority(
106106
self.authority_url, MinimalHttpClient())
107+
self.authority.initialize()
107108
self.scopes = ["s1", "s2"]
108109
self.uid = "my_uid"
109110
self.utid = "my_utid"

tests/test_authority.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def test_wellknown_host_and_tenant(self):
1313
for host in WELL_KNOWN_AUTHORITY_HOSTS:
1414
a = Authority(
1515
'https://{}/common'.format(host), MinimalHttpClient())
16+
a.initialize()
1617
self.assertEqual(
1718
a.authorization_endpoint,
1819
'https://%s/common/oauth2/v2.0/authorize' % host)
@@ -34,7 +35,7 @@ def test_unknown_host_wont_pass_instance_discovery(self):
3435
_assert = getattr(self, "assertRaisesRegex", self.assertRaisesRegexp) # Hack
3536
with _assert(ValueError, "invalid_instance"):
3637
Authority('https://example.com/tenant_doesnt_matter_in_this_case',
37-
MinimalHttpClient())
38+
MinimalHttpClient()).initialize()
3839

3940
def test_invalid_host_skipping_validation_can_be_turned_off(self):
4041
try:
@@ -85,7 +86,7 @@ def test_memorize(self):
8586
authority = "https://login.microsoftonline.com/common"
8687
self.assertNotIn(authority, Authority._domains_without_user_realm_discovery)
8788
a = Authority(authority, MinimalHttpClient(), validate_authority=False)
88-
89+
a.initialize()
8990
# We now pretend this authority supports no User Realm Discovery
9091
class MockResponse(object):
9192
status_code = 404

tests/test_authority_patch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def test_authority_honors_a_patched_requests(self):
1515
# First, we test that the original, unmodified authority is working
1616
a = msal.authority.Authority(
1717
"https://login.microsoftonline.com/common", MinimalHttpClient())
18+
a.initialize()
1819
self.assertEqual(
1920
a.authorization_endpoint,
2021
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
@@ -27,6 +28,7 @@ def test_authority_honors_a_patched_requests(self):
2728
with self.assertRaises(RuntimeError):
2829
a = msal.authority.Authority(
2930
"https://login.microsoftonline.com/common", MinimalHttpClient())
31+
a.initialize()
3032
finally: # Tricky:
3133
# Unpatch is necessary otherwise other test cases would be affected
3234
msal.authority.requests = original

0 commit comments

Comments
 (0)