Repository: AzureAD/azure-activedirectory-library-for-python

Issue:
retrieved token does not grant access to related resource (401 Error)
Hi, 
we stumbled over what we think can be categorized as a bug in the library. We are able to successfully obtain a token for a given resource and client id. However, the token does not grant us access to the resource. Whenever we call the resource with the `access_token`, we get a HTTP 401 response.

When we use the .Net library, with the exact same parameters, the token works.

The difference we have found is that the python library explicitly sends the parameter `api-version=1.0` to the oauth2 endpoint.

`POST https://login.microsoftonline.com/common/oauth2/token?api-version=1.0`

Once we changed the following line in `oauth2_client.py` in the adal library, I could access my resource.

`return urlparse('{}?{}'.format(self._token_endpoint, urlencode(parameters)))`

in the method `_create_token_url`, to

`return urlparse(self._token_endpoint)`.

See also this question on StackOverflow, the latest answer is from us.
http://stackoverflow.com/questions/37909332/401-unauthorized-making-rest-call-to-azure-api-app-using-bearer-token/ 

Comments:

Patch:
diff --git a/adal/authentication_context.py b/adal/authentication_context.py
index 9e8ac06..6557bf3 100644
--- a/adal/authentication_context.py
+++ b/adal/authentication_context.py
@@ -26,6 +26,7 @@
 #------------------------------------------------------------------------------
 import os
 import threading
+import warnings
 
 from .authority import Authority
 from . import argument
@@ -44,9 +45,11 @@ class AuthenticationContext(object):
         https://github.com/AzureAD/azure-activedirectory-library-for-python
     '''
 
-    def __init__(self, authority, validate_authority=None, cache=None):
-        '''Creates a new AuthenticationContext object. 
-        
+    def __init__(
+            self, authority, validate_authority=None, cache=None,
+            api_version='1.0'):
+        '''Creates a new AuthenticationContext object.
+
         By default the authority will be checked against a list of known Azure
         Active Directory authorities. If the authority is not recognized as 
         one of these well known authorities then token acquisition will fail.
@@ -62,13 +65,30 @@ class AuthenticationContext(object):
             the AuthenticationContext and are not shared unless it has been
             manually passed during the construction of other
             AuthenticationContexts.
+        :param api_version: (optional) Specifies API version using on the wire.
+            Historically it has a hardcoded default value as "1.0".
+            Developers are now encouraged to set it as None explicitly,
+            which means the underlying API version will be automatically chosen.
+            In next major release, this default value will be changed to None.
         '''
         self.authority = Authority(authority, validate_authority is None or validate_authority)
         self._oauth2client = None
         self.correlation_id = None
         env_value = os.environ.get('ADAL_PYTHON_SSL_NO_VERIFY')
+        if api_version is not None:
+            warnings.warn(
+                """The default behavior of including api-version=1.0 on the wire
+                is now deprecated.
+                Future version of ADAL will change the default value to None.
+
+                To ensure a smooth transition, you are recommended to explicitly
+                set it to None in your code now, and test out the new behavior.
+
+                    context = AuthenticationContext(..., api_version=None)
+                """, DeprecationWarning)
         self._call_context = {
             'options': GLOBAL_ADAL_OPTIONS,
+            'api_version': api_version,
             'verify_ssl': None if env_value is None else not env_value # mainly for tracing through proxy
             }
         self._token_requests_with_user_code = {}
diff --git a/adal/oauth2_client.py b/adal/oauth2_client.py
index a7d6e78..dd26c4a 100644
--- a/adal/oauth2_client.py
+++ b/adal/oauth2_client.py
@@ -105,7 +105,9 @@ class OAuth2Client(object):
 
     def _create_token_url(self):
         parameters = {}
-        parameters[OAuth2.Parameters.AAD_API_VERSION] = '1.0'
+        if self._call_context.get('api_version'):
+            parameters[OAuth2.Parameters.AAD_API_VERSION] = self._call_context[
+                'api_version']
 
         return urlparse('{}?{}'.format(self._token_endpoint, urlencode(parameters)))
 


Test patch:
diff --git a/tests/test_api_version.py b/tests/test_api_version.py
new file mode 100644
index 0000000..2a94873
--- /dev/null
+++ b/tests/test_api_version.py
@@ -0,0 +1,79 @@
+﻿#------------------------------------------------------------------------------
+#
+# Copyright (c) Microsoft Corporation.
+# All rights reserved.
+#
+# This code is licensed under the MIT License.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files(the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions :
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#------------------------------------------------------------------------------
+
+import warnings
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+import adal
+
+class TestAuthenticationContextApiVersionBehavior(unittest.TestCase):
+
+    def test_api_version_default_value(self):
+        with warnings.catch_warnings(record=True) as caught_warnings:
+            warnings.simplefilter("always")
+            context = adal.AuthenticationContext(
+                "https://login.windows.net/tenant")
+            self.assertEqual(context._call_context['api_version'], '1.0')
+            if len(caught_warnings) == 1:
+                # It should be len(caught_warnings)==1, but somehow it works on
+                # all my local test environment but not on Travis-CI.
+                # So we relax this check, for now.
+                self.assertIn("deprecated", str(caught_warnings[0].message))
+
+    def test_explicitly_turn_off_api_version(self):
+        with warnings.catch_warnings(record=True) as caught_warnings:
+            warnings.simplefilter("always")
+            context = adal.AuthenticationContext(
+                "https://login.windows.net/tenant", api_version=None)
+            self.assertEqual(context._call_context['api_version'], None)
+            self.assertEqual(len(caught_warnings), 0)
+
+class TestOAuth2ClientApiVersionBehavior(unittest.TestCase):
+
+    authority = mock.Mock(token_endpoint="https://example.com/token")
+
+    def test_api_version_is_set(self):
+        client = adal.oauth2_client.OAuth2Client(
+            {"api_version": "1.0", "log_context": mock.Mock()}, self.authority)
+        self.assertIn('api-version=1.0', client._create_token_url().geturl())
+
+    def test_api_version_is_not_set(self):
+        client = adal.oauth2_client.OAuth2Client(
+            {"api_version": None, "log_context": mock.Mock()}, self.authority)
+        self.assertNotIn('api-version=1.0', client._create_token_url().geturl())
+
+if __name__ == '__main__':
+    unittest.main()
+