From 11797eedf4b52c4d50079bb9cf34adbbba842a8f Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 17 Jul 2017 12:20:48 -0700 Subject: [PATCH 1/9] Include wheel format in next release --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c5bea7c..6003951e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ script: deploy: - # test pypi provider: pypi - distributions: sdist + distributions: "sdist bdist_wheel" server: https://test.pypi.org/legacy/ user: "nugetaad" password: @@ -28,7 +28,7 @@ deploy: - # production pypi provider: pypi - distributions: sdist + distributions: "sdist bdist_wheel" user: "nugetaad" password: secure: Wm2jGolFLm/wrfSPklf9gYdWiTK7ycGr+Qa0voVmFEJkW69PRC5bCibJI3POK1DqTBmQn7gi5G0s117PoLlXvK9lqwMaDL6Yf/ro7YnMU9pBopoB/zWMxWYZeBJVugmTGKuTkbUiQBzL2h0EnaQvvyrEDiLGrYrYEgLUSuR5AVTlvYKk1XBAAhvh8hu8JjgQQugN2ne6ZR9aBjCap0fzdTs3vhad/OQx+iH8YR8UTl4ruszdoL95CDtFmKdIkwg0qgIB65MqC6XAQ2tvhyMDHXZMMafE0NQwUwm2d+sqinCfHLNkb5bVBS0M8syrYCS8xr6Ccnt0PM1+nNFm83bu1w+HaMwKWD2IaU26QH8H7djc7mO1XmRmMSxQ1EjR313YyF534+uiLBlJWB8DOfN4r3/lqg6e44CY0impiT7NnT47bUqaoglew5HB0FgrrtGDrDlLa7zf+RHyb2BhqeqlTR1s0nnzsmzQMdxaHXvCbzYPqg3PUdwLHGBks90tXhA0zUg/3XQfb7v17Lx1byRufvsWWYXUZwLI6H8CCvWtWFvJ3TSPPBR/5LjaICVtt2g3Uv2xrG3weCIO52G7WQ6pIpOyiRsYkUAIXLi2UNsv4LlpNxNObNgL7FNfrNR/tEs8+SdbAkaf2jrFfn+Sk7v4pdPd4og7YXWAE2R/ge9nsJ4= From 6eefa62733a42d7b97bd83a51a2b52f2d213e31a Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 7 Aug 2017 15:21:57 -0700 Subject: [PATCH 2/9] Remove outdated code snippets in README And guide readers to the runnable samples. --- README.md | 65 ++++++------------------------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index b21a3d04..8a968466 100644 --- a/README.md +++ b/README.md @@ -30,71 +30,20 @@ For more context, starts with this [stackoverflow thread](http://stackoverflow.c In order to use this token acquisition method, you need to configure a service principal. Please follow [this walkthrough](https://azure.microsoft.com/en-us/documentation/articles/resource-group-create-service-principal-portal/). -See the [sample](./sample/client_credentials_sample.py). -```python -import adal - -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") -``` +See the [sample](sample/client_credentials_sample.py#L46-L53). ### Acquire Token with client certificate -A service principal is also required. See the [sample](./sample/certificate_credentials_sample.py). -```python -import adal -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_certificate( - RESOURCE, - "http://PythonSDK", - 'yourPrivateKeyFileContent', - 'thumbprintOfPrivateKey') -``` +A service principal is also required. See the [sample](sample/certificate_credentials_sample.py#L55-L62). ### Acquire Token with Refresh Token -See the [sample](./sample/refresh_token_sample.py). -```python -import adal -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_username_password( - RESOURCE, - 'yourName', - 'yourPassword', - 'yourClientIdHere') - -refresh_token = token['refreshToken'] -token = context.acquire_token_with_refresh_token( - refresh_token, - 'yourClientIdHere', - RESOURCE) -``` +See the [sample](sample/refresh_token_sample.py#L47-L64). ### Acquire Token with device code -See the [sample](./sample/device_code_sample.py). -```python -context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') -RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource -code = context.acquire_user_code(RESOURCE, 'yourClientIdHere') -print(code['message']) -token = context.acquire_token_with_device_code(RESOURCE, code, 'yourClientIdHere') -``` +See the [sample](sample/device_code_sample.py#L49-L52). + ### Acquire Token with authorization code -See the [sample](./sample/website_sample.py) for a complete bare bones web site that makes use of the code below. -```python -context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') -RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource -return context.acquire_token_with_authorization_code( - 'yourCodeFromQueryString', - 'yourWebRedirectUri', - RESOURCE, - 'yourClientId', - 'yourClientSecret') -``` +See the [sample](sample/website_sample.py#L107-L113) for a complete bare bones web site that makes use of the code below. + ## Samples and Documentation We provide a full suite of [sample applications on GitHub](https://github.com/azure-samples?utf8=%E2%9C%93&q=active-directory&type=&language=) and an [Azure AD developer landing page](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-developers-guide) to help you get started with learning the Azure Identity system. This includes tutorials for native clients and web applications. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect and for calling APIs such as the Graph API. From 0721870d3b28583ce9055f0ca3abc6b2347eb722 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 7 Aug 2017 17:51:53 -0700 Subject: [PATCH 3/9] README references the main logic part in sample code --- README.md | 11 ++++++----- sample/certificate_credentials_sample.py | 2 ++ sample/client_credentials_sample.py | 2 ++ sample/device_code_sample.py | 2 ++ sample/refresh_token_sample.py | 2 ++ sample/website_sample.py | 2 ++ 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8a968466..729ba00c 100644 --- a/README.md +++ b/README.md @@ -30,19 +30,20 @@ For more context, starts with this [stackoverflow thread](http://stackoverflow.c In order to use this token acquisition method, you need to configure a service principal. Please follow [this walkthrough](https://azure.microsoft.com/en-us/documentation/articles/resource-group-create-service-principal-portal/). -See the [sample](sample/client_credentials_sample.py#L46-L53). +Find the `Main logic` part in the [sample](sample/client_credentials_sample.py#L46-L55). ### Acquire Token with client certificate -A service principal is also required. See the [sample](sample/certificate_credentials_sample.py#L55-L62). +A service principal is also required. +Find the `Main logic` part in the [sample](sample/certificate_credentials_sample.py#L55-L64). ### Acquire Token with Refresh Token -See the [sample](sample/refresh_token_sample.py#L47-L64). +Find the `Main logic` part in the [sample](sample/refresh_token_sample.py#L47-L66). ### Acquire Token with device code -See the [sample](sample/device_code_sample.py#L49-L52). +Find the `Main logic` part in the [sample](sample/device_code_sample.py#L49-L54). ### Acquire Token with authorization code -See the [sample](sample/website_sample.py#L107-L113) for a complete bare bones web site that makes use of the code below. +Find the `Main logic` part in the [sample](sample/website_sample.py#L107-L115) for a complete bare bones web site that makes use of the code below. ## Samples and Documentation diff --git a/sample/certificate_credentials_sample.py b/sample/certificate_credentials_sample.py index aaadb4f2..5578f0d0 100644 --- a/sample/certificate_credentials_sample.py +++ b/sample/certificate_credentials_sample.py @@ -52,6 +52,7 @@ def get_private_key(filename): #uncomment for verbose logging turn_on_logging() +### Main logic begins context = adal.AuthenticationContext(authority_url, api_version=None) key = get_private_key(sample_parameters['privateKeyFile']) @@ -60,6 +61,7 @@ def get_private_key(filename): sample_parameters['clientId'], key, sample_parameters['thumbprint']) +### Main logic ends print('Here is the token:') print(json.dumps(token, indent=2)) diff --git a/sample/client_credentials_sample.py b/sample/client_credentials_sample.py index 57c6aa35..37cffd6e 100644 --- a/sample/client_credentials_sample.py +++ b/sample/client_credentials_sample.py @@ -43,6 +43,7 @@ def turn_on_logging(): #uncomment for verbose log #turn_on_logging() +### Main logic begins context = adal.AuthenticationContext( authority_url, validate_authority=sample_parameters['tenant'] != 'adfs', api_version=None) @@ -51,6 +52,7 @@ def turn_on_logging(): RESOURCE, sample_parameters['clientId'], sample_parameters['clientSecret']) +### Main logic ends print('Here is the token:') print(json.dumps(token, indent=2)) diff --git a/sample/device_code_sample.py b/sample/device_code_sample.py index 21728d94..3d9145b1 100644 --- a/sample/device_code_sample.py +++ b/sample/device_code_sample.py @@ -46,10 +46,12 @@ def turn_on_logging(): #uncomment for verbose logging #turn_on_logging() +### Main logic begins context = adal.AuthenticationContext(authority_url, api_version=None) code = context.acquire_user_code(RESOURCE, clientid) print(code['message']) token = context.acquire_token_with_device_code(RESOURCE, code, clientid) +### Main logic ends print('Here is the token from "{}":'.format(authority_url)) print(json.dumps(token, indent=2)) diff --git a/sample/refresh_token_sample.py b/sample/refresh_token_sample.py index d25b2f56..3379e47a 100644 --- a/sample/refresh_token_sample.py +++ b/sample/refresh_token_sample.py @@ -44,6 +44,7 @@ def turn_on_logging(): #uncomment for verbose log #turn_on_logging() +### Main logic begins context = adal.AuthenticationContext( authority_url, validate_authority=sample_parameters['tenant'] != 'adfs', api_version=None) @@ -62,6 +63,7 @@ def turn_on_logging(): refresh_token, sample_parameters['clientid'], RESOURCE) +### Main logic ends print('Here is the token acquired from the refreshing token') print(json.dumps(token, indent=2)) diff --git a/sample/website_sample.py b/sample/website_sample.py index 162610dc..fd57d66a 100644 --- a/sample/website_sample.py +++ b/sample/website_sample.py @@ -104,6 +104,7 @@ def _acquire_token(self): cookie = Cookie.SimpleCookie(self.headers["Cookie"]) if state != cookie['auth_state'].value: raise ValueError('state does not match') + ### Main logic begins auth_context = AuthenticationContext(authority_url, api_version=None) return auth_context.acquire_token_with_authorization_code( code, @@ -111,6 +112,7 @@ def _acquire_token(self): RESOURCE, sample_parameters['clientId'], sample_parameters['clientSecret']) + ### Main logic ends def _send_response(self, message, is_ok=True): self.send_response(200 if is_ok else 400) From cd0003a6f44385b40b280f99412ef4e3a235d223 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Wed, 9 Aug 2017 10:57:45 -0700 Subject: [PATCH 4/9] Clarify when client_secret is needed --- README.md | 2 +- sample/refresh_token_sample.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 729ba00c..3d45d538 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ A service principal is also required. Find the `Main logic` part in the [sample](sample/certificate_credentials_sample.py#L55-L64). ### Acquire Token with Refresh Token -Find the `Main logic` part in the [sample](sample/refresh_token_sample.py#L47-L66). +Find the `Main logic` part in the [sample](sample/refresh_token_sample.py#L47-L69). ### Acquire Token with device code Find the `Main logic` part in the [sample](sample/device_code_sample.py#L49-L54). diff --git a/sample/refresh_token_sample.py b/sample/refresh_token_sample.py index 3379e47a..0b5bfafb 100644 --- a/sample/refresh_token_sample.py +++ b/sample/refresh_token_sample.py @@ -62,7 +62,10 @@ def turn_on_logging(): token = context.acquire_token_with_refresh_token( refresh_token, sample_parameters['clientid'], - RESOURCE) + RESOURCE, + # client_secret="your_secret" # This is needed when using Confidential Client, + # otherwise you will encounter an invalid_client error. + ) ### Main logic ends print('Here is the token acquired from the refreshing token') From 121a8ec0b48f10b107973a2646ecc6f539d3aa2b Mon Sep 17 00:00:00 2001 From: yugangw-msft Date: Wed, 16 Aug 2017 16:46:47 -0700 Subject: [PATCH 5/9] candidate fix --- adal/cache_driver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/adal/cache_driver.py b/adal/cache_driver.py index 9683dca4..f5c01eca 100644 --- a/adal/cache_driver.py +++ b/adal/cache_driver.py @@ -132,6 +132,10 @@ def _create_entry_from_refresh(self, entry, refresh_response): new_entry = copy.deepcopy(entry) new_entry.update(refresh_response) + # for ADFS, it is possible the response payload has no 'resource' field, so we add it + if 'resource' not in refresh_response: + new_entry['resource'] = self._resource + if entry[TokenResponseFields.IS_MRRT] and self._authority != entry[TokenResponseFields._AUTHORITY]: new_entry[TokenResponseFields._AUTHORITY] = self._authority From 8cb74333eb80574b0633d59c2e8f1a977629a083 Mon Sep 17 00:00:00 2001 From: Yugang Wang Date: Fri, 18 Aug 2017 21:04:29 -0700 Subject: [PATCH 6/9] add comment --- adal/cache_driver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adal/cache_driver.py b/adal/cache_driver.py index f5c01eca..788cbe99 100644 --- a/adal/cache_driver.py +++ b/adal/cache_driver.py @@ -132,7 +132,9 @@ def _create_entry_from_refresh(self, entry, refresh_response): new_entry = copy.deepcopy(entry) new_entry.update(refresh_response) - # for ADFS, it is possible the response payload has no 'resource' field, so we add it + # It is possible the response payload has no 'resource' field, like in ADFS, so we manually + # fill it here. Note, 'resource' is part of the token cache key, so we have to set it to avoid + # corrupting the cache. if 'resource' not in refresh_response: new_entry['resource'] = self._resource From 82684e9aaf23069d56602a67585ba7b6cd0907d7 Mon Sep 17 00:00:00 2001 From: Yugang Wang Date: Mon, 21 Aug 2017 11:28:50 -0700 Subject: [PATCH 7/9] add a test --- tests/test_refresh_token.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_refresh_token.py b/tests/test_refresh_token.py index 3e19e069..90340457 100644 --- a/tests/test_refresh_token.py +++ b/tests/test_refresh_token.py @@ -67,5 +67,20 @@ def side_effect (tokenfunc): 'The response did not match what was expected: ' + str(token_response) ) + @httpretty.activate + def test_happy_path_with_resource_adfs(self): + tokenRequest = util.setup_expected_refresh_token_request_response(200, self.wire_response, self.response['authority'], self.response['resource'], cp['clientSecret']) + + context = adal.AuthenticationContext(cp['authorityTenant']) + def side_effect (tokenfunc): + return self.response['decodedResponse'] + + context._acquire_token = mock.MagicMock(side_effect=side_effect) + + token_response = context.acquire_token(cp['refreshToken'], cp['clientId'], cp['clientSecret'], cp['resource']) + self.assertTrue( + util.is_match_token_response(self.response['decodedResponse'], token_response), + 'The response did not match what was expected: ' + str(token_response) + ) if __name__ == '__main__': unittest.main() From aeeded625ed9315d18918b2f1e4787da03c38005 Mon Sep 17 00:00:00 2001 From: Yugang Wang Date: Mon, 21 Aug 2017 11:37:14 -0700 Subject: [PATCH 8/9] add a test --- tests/test_refresh_token.py | 51 ++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/tests/test_refresh_token.py b/tests/test_refresh_token.py index 90340457..4c958f62 100644 --- a/tests/test_refresh_token.py +++ b/tests/test_refresh_token.py @@ -41,46 +41,61 @@ import mock import adal -from adal.authentication_context import AuthenticationContext +from adal.authentication_context import AuthenticationContext, TokenCache from tests import util from tests.util import parameters as cp class TestRefreshToken(unittest.TestCase): - def setUp(self): - self.response_options = { 'refreshedRefresh' : True } - self.response = util.create_response(self.response_options) - self.wire_response = self.response['wireResponse'] @httpretty.activate def test_happy_path_with_resource_client_secret(self): - tokenRequest = util.setup_expected_refresh_token_request_response(200, self.wire_response, self.response['authority'], self.response['resource'], cp['clientSecret']) + response_options = { 'refreshedRefresh' : True } + response = util.create_response(response_options) + wire_response = response['wireResponse'] + tokenRequest = util.setup_expected_refresh_token_request_response(200, wire_response, response['authority'], response['resource'], cp['clientSecret']) context = adal.AuthenticationContext(cp['authorityTenant']) def side_effect (tokenfunc): - return self.response['decodedResponse'] + return response['decodedResponse'] context._acquire_token = mock.MagicMock(side_effect=side_effect) token_response = context.acquire_token_with_refresh_token(cp['refreshToken'], cp['clientId'], cp['clientSecret'], cp['resource']) self.assertTrue( - util.is_match_token_response(self.response['decodedResponse'], token_response), + util.is_match_token_response(response['decodedResponse'], token_response), 'The response did not match what was expected: ' + str(token_response) ) @httpretty.activate def test_happy_path_with_resource_adfs(self): - tokenRequest = util.setup_expected_refresh_token_request_response(200, self.wire_response, self.response['authority'], self.response['resource'], cp['clientSecret']) + # arrange + # set up token refresh result + wire_response = util.create_response({ + 'refreshedRefresh' : True, + 'mrrt': False + })['wireResponse'] + new_resource = 'https://graph.local.azurestack.external/' + tokenRequest = util.setup_expected_refresh_token_request_response(200, wire_response, cp['authority'], new_resource) - context = adal.AuthenticationContext(cp['authorityTenant']) - def side_effect (tokenfunc): - return self.response['decodedResponse'] + # set up an existing token to be used for refreshing + existing_token = util.create_response({ + 'refreshedRefresh': True, + 'mrrt': True + })['decodedResponse'] + existing_token['_clientId'] = existing_token.get('_clientId') or cp['clientId'] + existing_token['isMRRT'] = existing_token.get('isMRRT') or True + existing_token['_authority'] = existing_token.get('_authority') or cp['authorizeUrl'] + token_cache = TokenCache(json.dumps([existing_token])) - context._acquire_token = mock.MagicMock(side_effect=side_effect) + # act + user_id = existing_token['userId'] + context = adal.AuthenticationContext(cp['authorityTenant'], cache=token_cache) + token_response = context.acquire_token(new_resource, user_id, cp['clientId']) + + # assert + tokens = [value for key, value in token_cache.read_items()] + self.assertEqual(2, len(tokens)) + self.assertEqual({cp['resource'], new_resource}, set([x['resource'] for x in tokens])) - token_response = context.acquire_token(cp['refreshToken'], cp['clientId'], cp['clientSecret'], cp['resource']) - self.assertTrue( - util.is_match_token_response(self.response['decodedResponse'], token_response), - 'The response did not match what was expected: ' + str(token_response) - ) if __name__ == '__main__': unittest.main() From e80b83af26871d551abfa7bd5bf46804bee2d334 Mon Sep 17 00:00:00 2001 From: Ray Luo Date: Mon, 28 Aug 2017 12:16:17 -0700 Subject: [PATCH 9/9] ADAL Python 0.4.7 Bumping version number --- adal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adal/__init__.py b/adal/__init__.py index da939ffc..f73f038a 100644 --- a/adal/__init__.py +++ b/adal/__init__.py @@ -27,7 +27,7 @@ # pylint: disable=wrong-import-position -__version__ = '0.4.6' +__version__ = '0.4.7' import logging