From 0f67285f0afc5072134cb01b496e3cb807558154 Mon Sep 17 00:00:00 2001 From: Abhidnya Date: Mon, 18 May 2020 17:19:14 -0700 Subject: [PATCH 1/7] Changing format of links in docstring --- msal/application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/msal/application.py b/msal/application.py index c4f42c1e..096ee24e 100644 --- a/msal/application.py +++ b/msal/application.py @@ -284,8 +284,9 @@ def get_authorization_request_url( Can be one of "consumers" or "organizations" or your tenant domain "contoso.com". If included, it will skip the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience. - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8 + More information on possible values + `here `_ and + `here `_. :return: The authorization url as a string. """ """ # TBD: this would only be meaningful in a new acquire_token_interactive() From 3afb62b267db3a6f92f2a91d3212ad88c0b2d40a Mon Sep 17 00:00:00 2001 From: Neil Katin Date: Fri, 22 May 2020 19:15:27 -0700 Subject: [PATCH 2/7] One of the examples in the README was not legal python syntax. I updated the example to match what the source seems to want. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78d56abd..7702b367 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ There will be some variations for different flows. They are demonstrated in from msal import PublicClientApplication app = PublicClientApplication( "your_client_id", - "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here") + authority="https://login.microsoftonline.com/Enter_the_Tenant_Name_Here") ``` Later, each time you would want an access token, you start by: From 14ddb25119b7c8c6e7d952510635065375c51bd3 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Tue, 2 Jun 2020 16:06:37 -0700 Subject: [PATCH 3/7] Use release history as changelog for PyPI --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 11d9c44f..7e543541 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [bdist_wheel] universal=1 +[metadata] +project_urls = + Changelog = https://github.com/AzureAD/microsoft-authentication-library-for-python/releases From 78b77a23c1908fc2116713676d262c832ee1bbd0 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Fri, 19 Jun 2020 15:12:44 -0700 Subject: [PATCH 4/7] Migration should fail gracefully even on wrong RT --- msal/application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/msal/application.py b/msal/application.py index 096ee24e..e3bbc794 100644 --- a/msal/application.py +++ b/msal/application.py @@ -284,7 +284,7 @@ def get_authorization_request_url( Can be one of "consumers" or "organizations" or your tenant domain "contoso.com". If included, it will skip the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience. - More information on possible values + More information on possible values `here `_ and `here `_. :return: The authorization url as a string. @@ -726,9 +726,10 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes): """ return self.client.obtain_token_by_refresh_token( refresh_token, - decorate_scope(scopes, self.client_id), + scope=decorate_scope(scopes, self.client_id), rt_getter=lambda rt: rt, on_updating_rt=False, + on_removing_rt=lambda rt_item: None, # No OP ) From f2f25bc483651dda206ab85e8aa6d119f164cc4a Mon Sep 17 00:00:00 2001 From: Abhidnya Date: Mon, 22 Jun 2020 14:58:33 -0700 Subject: [PATCH 5/7] Fix typos (#210) --- msal/oauth2cli/oauth2.py | 6 +++--- msal/oauth2cli/oidc.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/msal/oauth2cli/oauth2.py b/msal/oauth2cli/oauth2.py index 3bf9339e..55fa0547 100644 --- a/msal/oauth2cli/oauth2.py +++ b/msal/oauth2cli/oauth2.py @@ -233,7 +233,7 @@ def obtain_token_by_refresh_token(self, refresh_token, scope=None, **kwargs): :param refresh_token: The refresh token issued to the client :param scope: If omitted, is treated as equal to the scope originally - granted by the resource ownser, + granted by the resource owner, according to https://tools.ietf.org/html/rfc6749#section-6 """ assert isinstance(refresh_token, string_types) @@ -397,7 +397,7 @@ def parse_auth_response(params, state=None): def obtain_token_by_authorization_code( self, code, redirect_uri=None, scope=None, **kwargs): - """Get a token via auhtorization code. a.k.a. Authorization Code Grant. + """Get a token via authorization code. a.k.a. Authorization Code Grant. This is typically used by a server-side app (Confidential Client), 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, Either way, this token_item will be passed into other callbacks as-is. :param scope: If omitted, is treated as equal to the scope originally - granted by the resource ownser, + granted by the resource owner, according to https://tools.ietf.org/html/rfc6749#section-6 :param rt_getter: A callable to translate the token_item to a raw RT string :param on_removing_rt: If absent, fall back to the one defined in initialization diff --git a/msal/oauth2cli/oidc.py b/msal/oauth2cli/oidc.py index 33bbdb2d..45861303 100644 --- a/msal/oauth2cli/oidc.py +++ b/msal/oauth2cli/oidc.py @@ -99,7 +99,7 @@ def build_auth_request_uri(self, response_type, nonce=None, **kwargs): response_type, nonce=nonce, **kwargs) def obtain_token_by_authorization_code(self, code, nonce=None, **kwargs): - """Get a token via auhtorization code. a.k.a. Authorization Code Grant. + """Get a token via authorization code. a.k.a. Authorization Code Grant. Return value and all other parameters are the same as :func:`oauth2.Client.obtain_token_by_authorization_code`, From 8d62027c9067a05a4c4d482bf659e1411f2a37f2 Mon Sep 17 00:00:00 2001 From: Abhidnya Date: Mon, 22 Jun 2020 15:41:31 -0700 Subject: [PATCH 6/7] Application initializer does not make tenant discovery calls (#205) --- msal/application.py | 40 +++++++++++++++++++++++------------ msal/authority.py | 11 ++++++++++ tests/test_application.py | 1 + tests/test_authority.py | 5 +++-- tests/test_authority_patch.py | 2 ++ 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/msal/application.py b/msal/application.py index 096ee24e..85cd6096 100644 --- a/msal/application.py +++ b/msal/application.py @@ -198,8 +198,9 @@ def __init__( authority or "https://login.microsoftonline.com/common/", self.http_client, validate_authority=validate_authority) # Here the self.authority is not the same type as authority in input + self.client = None self.token_cache = token_cache or TokenCache() - self.client = self._build_client(client_credential, self.authority) + self._client_credential = client_credential self.authority_groups = None def _build_client(self, client_credential, authority): @@ -248,6 +249,12 @@ def _build_client(self, client_credential, authority): on_removing_rt=self.token_cache.remove_rt, on_updating_rt=self.token_cache.update_rt) + def _get_client(self): + if not self.client: + self.authority.initialize() + self.client = self._build_client(self._client_credential, self.authority) + return self.client + def get_authorization_request_url( self, scopes, # type: list[str] @@ -307,6 +314,7 @@ def get_authorization_request_url( authority, self.http_client ) if authority else self.authority + the_authority.initialize() client = Client( {"authorization_endpoint": the_authority.authorization_endpoint}, @@ -367,7 +375,7 @@ def acquire_token_by_authorization_code( # really empty. assert isinstance(scopes, list), "Invalid parameter type" self._validate_ssh_cert_input_data(kwargs.get("data", {})) - return self.client.obtain_token_by_authorization_code( + return self._get_client().obtain_token_by_authorization_code( code, redirect_uri=redirect_uri, scope=decorate_scope(scopes, self.client_id), headers={ @@ -391,6 +399,7 @@ def get_accounts(self, username=None): Your app can choose to display those information to end user, and allow user to choose one of his/her accounts to proceed. """ + self.authority.initialize() accounts = self._find_msal_accounts(environment=self.authority.instance) if not accounts: # Now try other aliases of this authority instance for alias in self._get_authority_aliases(self.authority.instance): @@ -543,6 +552,7 @@ def acquire_token_silent_with_error( # authority, # self.http_client, # ) if authority else self.authority + self.authority.initialize() result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, self.authority, force_refresh=force_refresh, correlation_id=correlation_id, @@ -555,6 +565,7 @@ def acquire_token_silent_with_error( "https://" + alias + "/" + self.authority.tenant, self.http_client, validate_authority=False) + the_authority.initialize() result = self._acquire_token_silent_from_cache_and_possibly_refresh_it( scopes, account, the_authority, force_refresh=force_refresh, correlation_id=correlation_id, @@ -724,7 +735,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes): * A dict contains "error" and some other keys, when error happened. * A dict contains no "error" key means migration was successful. """ - return self.client.obtain_token_by_refresh_token( + return self._get_client().obtain_token_by_refresh_token( refresh_token, decorate_scope(scopes, self.client_id), rt_getter=lambda rt: rt, @@ -754,7 +765,7 @@ def initiate_device_flow(self, scopes=None, **kwargs): - an error response would contain some other readable key/value pairs. """ correlation_id = _get_new_correlation_id() - flow = self.client.initiate_device_flow( + flow = self._get_client().initiate_device_flow( scope=decorate_scope(scopes or [], self.client_id), headers={ CLIENT_REQUEST_ID: correlation_id, @@ -778,7 +789,7 @@ def acquire_token_by_device_flow(self, flow, **kwargs): - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". """ - return self.client.obtain_token_by_device_flow( + return self._get_client().obtain_token_by_device_flow( flow, data=dict(kwargs.pop("data", {}), code=flow["device_code"]), # 2018-10-4 Hack: @@ -815,6 +826,7 @@ def acquire_token_by_username_password( CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID), } + self.authority.initialize() if not self.authority.is_adfs: user_realm_result = self.authority.user_realm_discovery( username, correlation_id=headers[CLIENT_REQUEST_ID]) @@ -822,7 +834,7 @@ def acquire_token_by_username_password( return self._acquire_token_by_username_password_federated( user_realm_result, username, password, scopes=scopes, headers=headers, **kwargs) - return self.client.obtain_token_by_username_password( + return self._get_client().obtain_token_by_username_password( username, password, scope=scopes, headers=headers, **kwargs) @@ -851,16 +863,16 @@ def _acquire_token_by_username_password_federated( GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer' grant_type = { SAML_TOKEN_TYPE_V1: GRANT_TYPE_SAML1_1, - SAML_TOKEN_TYPE_V2: self.client.GRANT_TYPE_SAML2, + SAML_TOKEN_TYPE_V2: Client.GRANT_TYPE_SAML2, WSS_SAML_TOKEN_PROFILE_V1_1: GRANT_TYPE_SAML1_1, - WSS_SAML_TOKEN_PROFILE_V2: self.client.GRANT_TYPE_SAML2 + WSS_SAML_TOKEN_PROFILE_V2: Client.GRANT_TYPE_SAML2 }.get(wstrust_result.get("type")) if not grant_type: raise RuntimeError( "RSTR returned unknown token type: %s", wstrust_result.get("type")) - self.client.grant_assertion_encoders.setdefault( # Register a non-standard type - grant_type, self.client.encode_saml_assertion) - return self.client.obtain_token_by_assertion( + Client.grant_assertion_encoders.setdefault( # Register a non-standard type + grant_type, Client.encode_saml_assertion) + return self._get_client().obtain_token_by_assertion( wstrust_result["token"], grant_type, scope=scopes, **kwargs) @@ -878,7 +890,7 @@ def acquire_token_for_client(self, scopes, **kwargs): - an error response would contain "error" and usually "error_description". """ # TBD: force_refresh behavior - return self.client.obtain_token_for_client( + return self._get_client().obtain_token_for_client( scope=scopes, # This grant flow requires no scope decoration headers={ CLIENT_REQUEST_ID: _get_new_correlation_id(), @@ -910,9 +922,9 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs): """ # The implementation is NOT based on Token Exchange # https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16 - return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521 + return self._get_client().obtain_token_by_assertion( # bases on assertion RFC 7521 user_assertion, - self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs + Client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs scope=decorate_scope(scopes, self.client_id), # Decoration is used for: # 1. Explicitly requesting an RT, without relying on AAD default # behavior, even though it currently still issues an RT. diff --git a/msal/authority.py b/msal/authority.py index e200299d..edafbd3d 100644 --- a/msal/authority.py +++ b/msal/authority.py @@ -52,6 +52,17 @@ def __init__(self, authority_url, http_client, validate_authority=True): This parameter only controls whether an instance discovery will be performed. """ + self._http_client = http_client + self._authority_url = authority_url + self._validate_authority = validate_authority + self._is_initialized = False + + def initialize(self): + if not self._is_initialized: + self.__initialize(self._authority_url, self._http_client, self._validate_authority) + self._is_initialized = True + + def __initialize(self, authority_url, http_client, validate_authority): self._http_client = http_client authority, self.instance, tenant = canonicalize(authority_url) parts = authority.path.split('/') diff --git a/tests/test_application.py b/tests/test_application.py index 65b36b34..57095bbb 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -104,6 +104,7 @@ def setUp(self): self.authority_url = "https://login.microsoftonline.com/common" self.authority = msal.authority.Authority( self.authority_url, MinimalHttpClient()) + self.authority.initialize() self.scopes = ["s1", "s2"] self.uid = "my_uid" self.utid = "my_utid" diff --git a/tests/test_authority.py b/tests/test_authority.py index 15a0eb52..eae2c57a 100644 --- a/tests/test_authority.py +++ b/tests/test_authority.py @@ -13,6 +13,7 @@ def test_wellknown_host_and_tenant(self): for host in WELL_KNOWN_AUTHORITY_HOSTS: a = Authority( 'https://{}/common'.format(host), MinimalHttpClient()) + a.initialize() self.assertEqual( a.authorization_endpoint, 'https://%s/common/oauth2/v2.0/authorize' % host) @@ -34,7 +35,7 @@ def test_unknown_host_wont_pass_instance_discovery(self): _assert = getattr(self, "assertRaisesRegex", self.assertRaisesRegexp) # Hack with _assert(ValueError, "invalid_instance"): Authority('https://example.com/tenant_doesnt_matter_in_this_case', - MinimalHttpClient()) + MinimalHttpClient()).initialize() def test_invalid_host_skipping_validation_can_be_turned_off(self): try: @@ -85,7 +86,7 @@ def test_memorize(self): authority = "https://login.microsoftonline.com/common" self.assertNotIn(authority, Authority._domains_without_user_realm_discovery) a = Authority(authority, MinimalHttpClient(), validate_authority=False) - + a.initialize() # We now pretend this authority supports no User Realm Discovery class MockResponse(object): status_code = 404 diff --git a/tests/test_authority_patch.py b/tests/test_authority_patch.py index 1feca62d..0a211648 100644 --- a/tests/test_authority_patch.py +++ b/tests/test_authority_patch.py @@ -15,6 +15,7 @@ def test_authority_honors_a_patched_requests(self): # First, we test that the original, unmodified authority is working a = msal.authority.Authority( "https://login.microsoftonline.com/common", MinimalHttpClient()) + a.initialize() self.assertEqual( a.authorization_endpoint, 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize') @@ -27,6 +28,7 @@ def test_authority_honors_a_patched_requests(self): with self.assertRaises(RuntimeError): a = msal.authority.Authority( "https://login.microsoftonline.com/common", MinimalHttpClient()) + a.initialize() finally: # Tricky: # Unpatch is necessary otherwise other test cases would be affected msal.authority.requests = original From ec6432ba4205a73d7bc14bec770006272d802e3a Mon Sep 17 00:00:00 2001 From: Abhidnya Date: Thu, 25 Jun 2020 12:56:01 -0700 Subject: [PATCH 7/7] MSAL Python 1.4.0 Bumping version number --- msal/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal/application.py b/msal/application.py index aecfc5a8..24d45898 100644 --- a/msal/application.py +++ b/msal/application.py @@ -21,7 +21,7 @@ # The __init__.py will import this. Not the other way around. -__version__ = "1.3.0" +__version__ = "1.4.0" logger = logging.getLogger(__name__)