From 1ef292e1ae3f7f9715a490a4d708293d681df681 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 29 Nov 2016 11:56:47 -0800 Subject: [PATCH 1/8] Adding ADAL wrapper to msrestazure --- msrestazure/azure_active_directory.py | 37 +++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/msrestazure/azure_active_directory.py b/msrestazure/azure_active_directory.py index cda6808..01fd62e 100644 --- a/msrestazure/azure_active_directory.py +++ b/msrestazure/azure_active_directory.py @@ -39,10 +39,16 @@ MismatchingStateError, OAuth2Error, TokenExpiredError) -from requests import RequestException +from requests import ( + RequestException, + ConnectionError +) import requests_oauthlib as oauth -from msrest.authentication import OAuthTokenAuthentication +from msrest.authentication import ( + OAuthTokenAuthentication, + Authentication +) from msrest.exceptions import TokenExpiredError as Expired from msrest.exceptions import ( AuthenticationError, @@ -525,3 +531,30 @@ def set_token(self, response_url): raise_with_traceback(AuthenticationError, "", err) else: self.token = token + +class AdalAuthentication(Authentication):#pylint: disable=too-few-public-methods + + def __init__(self, token_retriever): + self._token_retriever = token_retriever + + def signed_session(self): + session = super(AdalAuthentication, self).signed_session() + + import adal # Adal is not mandatory + + try: + raw_token = self._token_retriever() + scheme, token = raw_token['tokenType'], raw_token['accessToken'] + except adal.AdalError as err: + #pylint: disable=no-member + if (hasattr(err, 'error_response') and ('error_description' in err.error_response) + and ('AADSTS70008:' in err.error_response['error_description'])): + raise Expired("Credentials have expired due to inactivity. Please run 'az login'") + + raise AuthenticationError(err) + except ConnectionError as err: + raise AuthenticationError('Please ensure you have network connection. Error detail: ' + str(err)) + + header = "{} {}".format(scheme, token) + session.headers['Authorization'] = header + return session \ No newline at end of file From 81864b95af405df983230ee8326b31b080d92193 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 29 Nov 2016 12:30:56 -0800 Subject: [PATCH 2/8] Add args/kwargs syntax feature to AdalAuthentication --- msrestazure/azure_active_directory.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/msrestazure/azure_active_directory.py b/msrestazure/azure_active_directory.py index 01fd62e..5187d5c 100644 --- a/msrestazure/azure_active_directory.py +++ b/msrestazure/azure_active_directory.py @@ -534,8 +534,10 @@ def set_token(self, response_url): class AdalAuthentication(Authentication):#pylint: disable=too-few-public-methods - def __init__(self, token_retriever): - self._token_retriever = token_retriever + def __init__(self, adal_method, *args, **kwargs): + self._adal_method = adal_method + self._args = args + self._kwargs = kwargs def signed_session(self): session = super(AdalAuthentication, self).signed_session() @@ -543,7 +545,7 @@ def signed_session(self): import adal # Adal is not mandatory try: - raw_token = self._token_retriever() + raw_token = self._adal_method(*self._args, **self._kwargs) scheme, token = raw_token['tokenType'], raw_token['accessToken'] except adal.AdalError as err: #pylint: disable=no-member From 5e482e85ffb10a0f04218c8b32ce36308640e944 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 29 Nov 2016 13:44:48 -0800 Subject: [PATCH 3/8] Remove az login message in Expired --- msrestazure/azure_active_directory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msrestazure/azure_active_directory.py b/msrestazure/azure_active_directory.py index 5187d5c..aa5d659 100644 --- a/msrestazure/azure_active_directory.py +++ b/msrestazure/azure_active_directory.py @@ -551,7 +551,7 @@ def signed_session(self): #pylint: disable=no-member if (hasattr(err, 'error_response') and ('error_description' in err.error_response) and ('AADSTS70008:' in err.error_response['error_description'])): - raise Expired("Credentials have expired due to inactivity. Please run 'az login'") + raise Expired("Credentials have expired due to inactivity.") raise AuthenticationError(err) except ConnectionError as err: From 0718a6cc5a3c277ee9e659ae3fd2fe3b71f97436 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 29 Nov 2016 17:16:18 -0800 Subject: [PATCH 4/8] Add Adal tests --- test/unittest_auth.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/unittest_auth.py b/test/unittest_auth.py index 363b4b8..93d403c 100644 --- a/test/unittest_auth.py +++ b/test/unittest_auth.py @@ -34,6 +34,7 @@ from requests_oauthlib import OAuth2Session import oauthlib +import adal from msrestazure import AzureConfiguration from msrestazure import azure_active_directory @@ -41,12 +42,14 @@ AADMixin, InteractiveCredentials, ServicePrincipalCredentials, - UserPassCredentials + UserPassCredentials, + AdalAuthentication ) from msrest.exceptions import ( TokenExpiredError, AuthenticationError, ) +from requests import ConnectionError class TestInteractiveCredentials(unittest.TestCase): @@ -356,6 +359,34 @@ def test_user_pass_credentials(self): client_id="client_id", username='my_username', password='my_password', resource='https://management.core.chinacloudapi.cn/', verify=False) + def test_adal_authentication(self): + def success_auth(): + return { + 'tokenType': 'https', + 'accessToken': 'cryptictoken' + } + + credentials = AdalAuthentication(success_auth) + session = credentials.signed_session() + self.assertEquals(session.headers['Authorization'], 'https cryptictoken') + + def error(): + raise adal.AdalError("You hacker", {}) + credentials = AdalAuthentication(error) + with self.assertRaises(AuthenticationError) as cm: + session = credentials.signed_session() + + def expired(): + raise adal.AdalError("Too late", {'error_description': "AADSTS70008: Expired"}) + credentials = AdalAuthentication(expired) + with self.assertRaises(TokenExpiredError) as cm: + session = credentials.signed_session() + + def connection_error(): + raise ConnectionError("Plug the network") + credentials = AdalAuthentication(connection_error) + with self.assertRaises(AuthenticationError) as cm: + session = credentials.signed_session() if __name__ == '__main__': unittest.main() \ No newline at end of file From fc316fcf30b959abf73dafe20e10f76636f6c80e Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 29 Nov 2016 17:16:55 -0800 Subject: [PATCH 5/8] Add Adal in dependencies --- requirements.txt | 1 + setup.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 366535d..b200cc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ msrest>=0.4.4,<0.5.0 +adal~=0.4.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 57a5bf5..9c44ccf 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,7 @@ 'License :: OSI Approved :: MIT License', 'Topic :: Software Development'], install_requires=[ - "msrest>=0.4.4"], + "msrest~=0.4.4", + "adal~=0.4.0" + ], ) From 24d71059b14e8b2c612cce35a98bfd603a14da6a Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 30 Nov 2016 09:27:34 -0800 Subject: [PATCH 6/8] Brett review on AdalAuthentication --- msrestazure/azure_active_directory.py | 80 ++++++++++++++++++++------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/msrestazure/azure_active_directory.py b/msrestazure/azure_active_directory.py index aa5d659..b5e70e2 100644 --- a/msrestazure/azure_active_directory.py +++ b/msrestazure/azure_active_directory.py @@ -33,26 +33,19 @@ from urllib.parse import urlparse, parse_qs import keyring +import adal from oauthlib.oauth2 import BackendApplicationClient, LegacyApplicationClient from oauthlib.oauth2.rfc6749.errors import ( InvalidGrantError, MismatchingStateError, OAuth2Error, TokenExpiredError) -from requests import ( - RequestException, - ConnectionError -) +from requests import RequestException, ConnectionError import requests_oauthlib as oauth -from msrest.authentication import ( - OAuthTokenAuthentication, - Authentication -) +from msrest.authentication import OAuthTokenAuthentication, Authentication from msrest.exceptions import TokenExpiredError as Expired -from msrest.exceptions import ( - AuthenticationError, - raise_with_traceback) +from msrest.exceptions import AuthenticationError, raise_with_traceback def _build_url(uri, paths, scheme): @@ -532,31 +525,80 @@ def set_token(self, response_url): else: self.token = token -class AdalAuthentication(Authentication):#pylint: disable=too-few-public-methods +class AdalAuthentication(Authentication): # pylint: disable=too-few-public-methods + """A wrapper to use ADAL for Python easily to authenticate on Azure. + """ def __init__(self, adal_method, *args, **kwargs): + """Take an ADAL `acquire_token` method and its parameters. + + For example, this code from the ADAL tutorial: + + ```python + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + token = context.acquire_token_with_client_credentials( + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal") + ``` + + can be written here: + + ```python + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + credentials = AdalAuthentication( + context.acquire_token_with_client_credentials, + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal") + ``` + + or using a lambda if you prefer: + + ```python + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + credentials = AdalAuthentication( + lambda: context.acquire_token_with_client_credentials( + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal" + ) + ) + ``` + + :param adal_method: A lambda with no args, or `acquire_token` method with args using args/kwargs + :param args: Optional args for the method + :param kwargs: Optional kwargs for the method + """ self._adal_method = adal_method self._args = args self._kwargs = kwargs def signed_session(self): - session = super(AdalAuthentication, self).signed_session() + """Get a signed session for requests. + + Usually called by the Azure SDKs for you to authenticate queries. - import adal # Adal is not mandatory + :rtype: requests.Session + """ + session = super(AdalAuthentication, self).signed_session() try: raw_token = self._adal_method(*self._args, **self._kwargs) - scheme, token = raw_token['tokenType'], raw_token['accessToken'] except adal.AdalError as err: - #pylint: disable=no-member + # pylint: disable=no-member if (hasattr(err, 'error_response') and ('error_description' in err.error_response) and ('AADSTS70008:' in err.error_response['error_description'])): raise Expired("Credentials have expired due to inactivity.") - - raise AuthenticationError(err) + else: + raise AuthenticationError(err) except ConnectionError as err: raise AuthenticationError('Please ensure you have network connection. Error detail: ' + str(err)) + scheme, token = raw_token['tokenType'], raw_token['accessToken'] header = "{} {}".format(scheme, token) session.headers['Authorization'] = header - return session \ No newline at end of file + return session From 72cd1575bd9c8475589c2f627f67fd8540c1a53e Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 30 Nov 2016 10:09:39 -0800 Subject: [PATCH 7/8] Brett review on test --- test/unittest_auth.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/unittest_auth.py b/test/unittest_auth.py index 93d403c..871c882 100644 --- a/test/unittest_auth.py +++ b/test/unittest_auth.py @@ -44,11 +44,8 @@ ServicePrincipalCredentials, UserPassCredentials, AdalAuthentication - ) -from msrest.exceptions import ( - TokenExpiredError, - AuthenticationError, - ) +) +from msrest.exceptions import TokenExpiredError, AuthenticationError from requests import ConnectionError @@ -389,4 +386,4 @@ def connection_error(): session = credentials.signed_session() if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 82532cf7a7ffb4dfbf9c8404dfa792bee70eb8ce Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Wed, 30 Nov 2016 10:10:40 -0800 Subject: [PATCH 8/8] Brett review fixes --- msrestazure/azure_active_directory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msrestazure/azure_active_directory.py b/msrestazure/azure_active_directory.py index b5e70e2..c8e52a3 100644 --- a/msrestazure/azure_active_directory.py +++ b/msrestazure/azure_active_directory.py @@ -527,8 +527,8 @@ def set_token(self, response_url): class AdalAuthentication(Authentication): # pylint: disable=too-few-public-methods - """A wrapper to use ADAL for Python easily to authenticate on Azure. - """ + """A wrapper to use ADAL for Python easily to authenticate on Azure.""" + def __init__(self, adal_method, *args, **kwargs): """Take an ADAL `acquire_token` method and its parameters.