Repository: wndhydrnt/python-oauth2

Issue:
New parameter client 
**AuthenticatingSiteAdapter.authenticate** and **UserFacingSiteAdapter.render_auth_page** could have a parameter to receive the client retrieved from the store.

In my case, my client table has extra columns that I need to render the authentication page, If I had access to the client object it would be easier. 

Thanks!

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f15bc9e..b5fcbb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Improvements:
   - Move WSGI server code into its own module ([@wndhydrnt][])
   - Renamed class acting as entrypoint for WSGI server from 'Server' to 'Application' ([@wndhydrnt][])
   - Client Credentials Grant example ([@shupp][])
+  - Methods `authenticate` and `render_auth_page` of a Site Adapter accept an instance of `oauth2.datatype.Client` ([@wndhydrnt][])
 
 Bugfixes:
 
diff --git a/README.rst b/README.rst
index 0b77e9b..a745d83 100644
--- a/README.rst
+++ b/README.rst
@@ -42,14 +42,15 @@ Example Authorization server
     # This can be used to display confirmation dialogs and the like.
     class ExampleSiteAdapter(oauth2.web.AuthorizationCodeGrantSiteAdapter,
                              oauth2.web.ImplicitGrantSiteAdapter):
-        def authenticate(self, request, environ, scopes):
+        def authenticate(self, request, environ, scopes, client):
             # Check if the user has granted access
             if request.post_param("confirm") == "confirm":
                 return {}
 
             raise oauth2.error.UserNotAuthenticated
 
-        def render_auth_page(self, request, response, environ, scopes):
+        def render_auth_page(self, request, response, environ, scopes,
+                             client):
             response.body = '''
     <html>
         <body>
diff --git a/docs/examples/authorization_code_grant.py b/docs/examples/authorization_code_grant.py
index a8ecf82..ff82b4c 100644
--- a/docs/examples/authorization_code_grant.py
+++ b/docs/examples/authorization_code_grant.py
@@ -68,12 +68,12 @@ class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter):
 </html>
     """
 
-    def render_auth_page(self, request, response, environ, scopes):
+    def render_auth_page(self, request, response, environ, scopes, client):
         response.body = self.CONFIRMATION_TEMPLATE
 
         return response
 
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         if request.method == "POST":
             if request.post_param("confirm") == "1":
                 return
diff --git a/docs/examples/base_server.py b/docs/examples/base_server.py
index be43cb5..27ac9b1 100644
--- a/docs/examples/base_server.py
+++ b/docs/examples/base_server.py
@@ -11,14 +11,14 @@ import oauth2.web.wsgi
 # This can be used to display confirmation dialogs and the like.
 class ExampleSiteAdapter(oauth2.web.AuthorizationCodeGrantSiteAdapter,
                          oauth2.web.ImplicitGrantSiteAdapter):
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         # Check if the user has granted access
         if request.post_param("confirm") == "confirm":
             return {}
 
         raise oauth2.error.UserNotAuthenticated
 
-    def render_auth_page(self, request, response, environ, scopes):
+    def render_auth_page(self, request, response, environ, scopes, client):
         response.body = '''
 <html>
     <body>
diff --git a/docs/examples/implicit_grant.py b/docs/examples/implicit_grant.py
index 4368f98..0d244d3 100644
--- a/docs/examples/implicit_grant.py
+++ b/docs/examples/implicit_grant.py
@@ -40,13 +40,13 @@ class TestSiteAdapter(ImplicitGrantSiteAdapter):
 </html>
     """
 
-    def render_auth_page(self, request, response, environ, scopes):
+    def render_auth_page(self, request, response, environ, scopes, client):
         # Add check if the user is logged or a redirect to the login page here
         response.body = self.CONFIRMATION_TEMPLATE
 
         return response
 
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         if request.method == "POST":
             if request.post_param("confirm") == "1":
                 return
diff --git a/docs/examples/resource_owner_grant.py b/docs/examples/resource_owner_grant.py
index 16d7990..d539ee6 100644
--- a/docs/examples/resource_owner_grant.py
+++ b/docs/examples/resource_owner_grant.py
@@ -157,7 +157,7 @@ class ClientApplication(object):
 
 
 class TestSiteAdapter(ResourceOwnerGrantSiteAdapter):
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         username = request.post_param("username")
         password = request.post_param("password")
         # A real world application could connect to a database, try to
diff --git a/docs/examples/tornado_server.py b/docs/examples/tornado_server.py
index 869be94..f9a77b6 100644
--- a/docs/examples/tornado_server.py
+++ b/docs/examples/tornado_server.py
@@ -70,12 +70,12 @@ class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter):
 </html>
     """
 
-    def render_auth_page(self, request, response, environ, scopes):
+    def render_auth_page(self, request, response, environ, scopes, client):
         response.body = self.CONFIRMATION_TEMPLATE
 
         return response
 
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         if request.method == "POST":
             if request.post_param("confirm") == "1":
                 return
diff --git a/docs/migration.rst b/docs/migration.rst
index 218db95..c4ccf14 100644
--- a/docs/migration.rst
+++ b/docs/migration.rst
@@ -83,3 +83,32 @@ After:
     # Instantiating storage and provider...
 
     app = Application(provider=provider)
+
+
+Client passed to methods authenticate and render_auth_page of a Site Adapter
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Before:
+
+.. code-block:: python
+
+    class ExampleSiteAdapter(AuthenticatingSiteAdapter, UserFacingSiteAdapter):
+        def authenticate(self, request, environ, scopes):
+            # code
+
+        def render_auth_page(self, request, response, environ, scopes):
+            # code
+
+
+After:
+
+.. code-block:: python
+
+    class ExampleSiteAdapter(AuthenticatingSiteAdapter, UserFacingSiteAdapter):
+        def authenticate(self, request, environ, scopes, client):
+            # code
+
+        def render_auth_page(self, request, response, environ, scopes, client):
+            # code
+
+
diff --git a/oauth2/grant.py b/oauth2/grant.py
index a792edd..1407af0 100644
--- a/oauth2/grant.py
+++ b/oauth2/grant.py
@@ -306,12 +306,14 @@ class AuthorizeMixin(object):
                                     explanation="Authorization denied by user")
 
         try:
-            result = self.site_adapter.authenticate(request, environ, scopes)
+            result = self.site_adapter.authenticate(request, environ, scopes,
+                                                    self.client)
 
             return self.sanitize_return_value(result)
         except UserNotAuthenticated:
             return self.site_adapter.render_auth_page(request, response,
-                                                      environ, scopes)
+                                                      environ, scopes,
+                                                      self.client)
 
     @staticmethod
     def sanitize_return_value(value):
@@ -816,7 +818,8 @@ class ResourceOwnerGrantHandler(GrantHandler, AccessTokenMixin):
         """
         try:
             data = self.site_adapter.authenticate(request, environ,
-                                                  self.scope_handler.scopes)
+                                                  self.scope_handler.scopes,
+                                                  self.client)
             data = AuthorizeMixin.sanitize_return_value(data)
         except UserNotAuthenticated:
             raise OAuthInvalidError(error="invalid_client",
diff --git a/oauth2/web/__init__.py b/oauth2/web/__init__.py
index 279692e..4e98019 100644
--- a/oauth2/web/__init__.py
+++ b/oauth2/web/__init__.py
@@ -6,7 +6,7 @@ class AuthenticatingSiteAdapter(object):
     """
     Extended by site adapters that need to authenticate the user.
     """
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         """
         Authenticates a user and checks if she has authorized access.
 
@@ -20,6 +20,9 @@ class AuthenticatingSiteAdapter(object):
                        scope.
         :type scopes: list
 
+        :param client: The client that initiated the authorization process
+        :type client: oauth2.datatype.Client
+
         :return: A ``dict`` containing arbitrary data that will be passed to
                  the current storage adapter and saved with auth code and
                  access token. Return a tuple in the form
@@ -40,7 +43,7 @@ class UserFacingSiteAdapter(object):
     Display HTML or redirect the user agent to another page of your website
     where she can do something before being returned to the OAuth 2.0 server.
     """
-    def render_auth_page(self, request, response, environ, scopes):
+    def render_auth_page(self, request, response, environ, scopes, client):
         """
         Defines how to display a confirmation page to the user.
 
@@ -57,6 +60,9 @@ class UserFacingSiteAdapter(object):
                        scope.
         :type scopes: list
 
+        :param client: The client that initiated the authorization process
+        :type client: oauth2.datatype.Client
+
         :return: The response passed in as a parameter.
                  It can contain HTML or issue a redirect.
         :rtype: oauth2.web.Response
diff --git a/setup.py b/setup.py
index f026e85..4200b67 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,4 @@
+import os
 import sys
 
 from setuptools import setup
@@ -15,8 +16,7 @@ setup(name="python-oauth2",
       author="Markus Meyer",
       author_email="hydrantanderwand@gmail.com",
       url="https://github.com/wndhydrnt/python-oauth2",
-      packages=["oauth2", "oauth2.web", "oauth2.store", "oauth2.store.dbapi",
-                "oauth2.test"],
+      packages=[d[0].replace("/", ".") for d in os.walk("oauth2") if not d[0].endswith("__pycache__")],
       extras_require={
         "memcache": [memcache_require],
         "mongodb": ["pymongo"],


Test patch:
diff --git a/oauth2/test/functional/test_authorization_code.py b/oauth2/test/functional/test_authorization_code.py
index 6706ad2..bd2ef3a 100644
--- a/oauth2/test/functional/test_authorization_code.py
+++ b/oauth2/test/functional/test_authorization_code.py
@@ -203,7 +203,7 @@ class AuthorizationCodeTestCase(unittest.TestCase):
 
 
 class TestSiteAdapter(AuthorizationCodeGrantSiteAdapter):
-    def authenticate(self, request, environ, scopes):
+    def authenticate(self, request, environ, scopes, client):
         return {"additional": "data"}, 1
 
     def user_has_denied_access(self, request):
diff --git a/oauth2/test/test_grant.py b/oauth2/test/test_grant.py
index e3f80b8..cfbb05b 100644
--- a/oauth2/test/test_grant.py
+++ b/oauth2/test/test_grant.py
@@ -149,6 +149,8 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         """
         AuthorizeMixin.authorize should return a tuple even if the SiteAdapter returns a dict
         """
+        client_mock = Mock(spec=Client)
+
         test_data = {"test": "data"}
 
         site_adapter_mock = Mock(spec=ImplicitGrantSiteAdapter)
@@ -156,6 +158,7 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         site_adapter_mock.authenticate.return_value = test_data
 
         auth_mixin = AuthorizeMixin(site_adapter=site_adapter_mock)
+        auth_mixin.client = client_mock
         result = auth_mixin.authorize(Mock(spec=Request), Mock(spec=Response),
                                       environ={}, scopes=[])
 
@@ -167,6 +170,8 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         """
         AuthorizeMixin.authorize should return the tuple returned by the SiteAdapter
         """
+        client_mock = Mock(spec=Client)
+
         test_data = ({"test": "data"}, 123)
 
         site_adapter_mock = Mock(spec=ImplicitGrantSiteAdapter)
@@ -174,6 +179,7 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         site_adapter_mock.authenticate.return_value = test_data
 
         auth_mixin = AuthorizeMixin(site_adapter=site_adapter_mock)
+        auth_mixin.client = client_mock
         result = auth_mixin.authorize(Mock(spec=Request), Mock(spec=Response),
                                       environ={}, scopes=[])
 
@@ -182,6 +188,8 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         self.assertEqual(result[1], test_data[1])
 
     def test_authorize_user_not_authenticated(self):
+        client_mock = Mock(spec=Client)
+
         response_mock = Mock(spec=Response)
 
         site_adapter_mock = Mock(spec=ImplicitGrantSiteAdapter)
@@ -190,6 +198,7 @@ class AuthorizeMixinTestCase(unittest.TestCase):
         site_adapter_mock.render_auth_page.return_value = response_mock
 
         auth_mixin = AuthorizeMixin(site_adapter=site_adapter_mock)
+        auth_mixin.client = client_mock
         result = auth_mixin.authorize(Mock(spec=Request), response_mock,
                                       environ={}, scopes=[])
 
@@ -209,6 +218,9 @@ class AuthorizationCodeAuthHandlerTestCase(unittest.TestCase):
 
         auth_code_store_mock = Mock(spec=AuthCodeStore)
 
+        client = Client(identifier="abc", secret="xyz",
+                        redirect_uris=[redirect_uri])
+
         response_mock = Mock(spec=Response)
 
         request_mock = Mock(spec=Request)
@@ -231,14 +243,14 @@ class AuthorizationCodeAuthHandlerTestCase(unittest.TestCase):
             token_generator=token_generator_mock
         )
 
-        handler.client = Client(identifier="abc", secret="xyz",
-                                redirect_uris=[redirect_uri])
+        handler.client = client
         handler.state = state
         response = handler.process(request_mock, response_mock, environ)
 
         token_generator_mock.generate.assert_called_with()
         site_adapter_mock.authenticate.assert_called_with(request_mock,
-                                                          environ, scopes)
+                                                          environ, scopes,
+                                                          client)
         self.assertTrue(auth_code_store_mock.save_code.called)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.body, "")
@@ -252,6 +264,8 @@ class AuthorizationCodeAuthHandlerTestCase(unittest.TestCase):
         response_mock = Mock(spec=Response)
         scopes = ["scopes"]
 
+        client_mock = Mock(spec=Client)
+
         request_mock = Mock(spec=Request)
 
         scope_handler_mock = Mock(Scope)
@@ -266,12 +280,14 @@ class AuthorizationCodeAuthHandlerTestCase(unittest.TestCase):
             scope_handler=scope_handler_mock, site_adapter=site_adapter_mock,
             token_generator=Mock()
         )
+        handler.client = client_mock
         response = handler.process(request_mock, response_mock, environ)
 
         site_adapter_mock.render_auth_page.assert_called_with(request_mock,
                                                               response_mock,
                                                               environ,
-                                                              scopes)
+                                                              scopes,
+                                                              client_mock)
         self.assertEqual(response, response_mock)
 
     def test_redirect_oauth_error(self):
@@ -801,7 +817,6 @@ class ImplicitGrantTestCase(unittest.TestCase):
 
 class ImplicitGrantHandlerTestCase(unittest.TestCase):
     def test_process_redirect_with_token(self):
-        client_id = "abc"
         environ = {"session": "data"}
         redirect_uri = "http://callback"
         scopes = ["scopes"]
@@ -810,6 +825,9 @@ class ImplicitGrantHandlerTestCase(unittest.TestCase):
 
         access_token_store_mock = Mock(spec=AccessTokenStore)
 
+        client = Client(identifier="abc", secret="xyz",
+                        redirect_uris=[redirect_uri])
+
         request_mock = Mock(spec=Request)
 
         responseMock = Mock(spec=Response)
@@ -831,12 +849,12 @@ class ImplicitGrantHandlerTestCase(unittest.TestCase):
             client_authenticator=Mock(), scope_handler=scope_handler_mock,
             site_adapter=site_adapter_mock,
             token_generator=token_generator_mock)
-        handler.client = Client(identifier="abc", secret="xyz",
-                                redirect_uris=[redirect_uri])
+        handler.client = client
         result_response = handler.process(request_mock, responseMock, environ)
 
         site_adapter_mock.authenticate.assert_called_with(request_mock,
-                                                          environ, scopes)
+                                                          environ, scopes,
+                                                          client)
 
         access_token, = access_token_store_mock.save_token.call_args[0]
         self.assertTrue(isinstance(access_token, AccessToken))
@@ -934,6 +952,8 @@ class ImplicitGrantHandlerTestCase(unittest.TestCase):
         scopes = ["scopes"]
         environ = {"session": "data"}
 
+        client_mock = Mock(spec=Client)
+
         request_mock = Mock(spec=Request)
 
         response_mock = Mock(spec=Response)
@@ -950,14 +970,17 @@ class ImplicitGrantHandlerTestCase(unittest.TestCase):
             scope_handler=scope_handler_mock, site_adapter=site_adapter_mock,
             token_generator=Mock()
         )
+        handler.client = client_mock
         result_response = handler.process(request_mock, response_mock, environ)
 
         site_adapter_mock.authenticate.assert_called_with(request_mock,
-                                                          environ, scopes)
+                                                          environ, scopes,
+                                                          client_mock)
         site_adapter_mock.render_auth_page.assert_called_with(request_mock,
                                                               response_mock,
                                                               environ,
-                                                              scopes)
+                                                              scopes,
+                                                              client_mock)
         self.assertEqual(result_response, response_mock)
 
     def test_process_user_denied_access(self):
@@ -1057,6 +1080,7 @@ class ResourceOwnerGrantTestCase(unittest.TestCase):
 class ResourceOwnerGrantHandlerTestCase(unittest.TestCase):
     def test_process(self):
         access_token = "0aef"
+        client = Client(identifier="abc", secret="xyz")
         expected_response_body = {"access_token": access_token,
                                   "token_type": "Bearer"}
         scopes = ["scope"]
@@ -1085,11 +1109,11 @@ class ResourceOwnerGrantHandlerTestCase(unittest.TestCase):
             scope_handler=scope_handler_mock,
             site_adapter=site_adapter_mock,
             token_generator=token_generator_mock)
-        handler.client = Client(identifier="abc", secret="xyz")
+        handler.client = client
         result = handler.process(request_mock, response_mock, {})
 
         site_adapter_mock.authenticate.assert_called_with(request_mock, {},
-                                                          scopes)
+                                                          scopes, client)
         token_generator_mock.create_access_token_data.assert_called_with(ResourceOwnerGrant.grant_type)
         access_token, = access_token_store_mock.save_token.call_args[0]
         self.assertTrue(isinstance(access_token, AccessToken))
@@ -1107,6 +1131,7 @@ class ResourceOwnerGrantHandlerTestCase(unittest.TestCase):
     @patch("time.time", mock_time)
     def test_process_with_refresh_token(self):
         access_token = "0aef"
+        client = Client(identifier="abc", secret="xyz")
         expected_response_body = {"access_token": access_token,
                                   "token_type": "Bearer",
                                   "refresh_token": "wxyz", "expires_in": 600}
@@ -1138,11 +1163,11 @@ class ResourceOwnerGrantHandlerTestCase(unittest.TestCase):
             scope_handler=scope_handler_mock,
             site_adapter=site_adapter_mock,
             token_generator=token_generator_mock)
-        handler.client = Client(identifier="abc", secret="xyz")
+        handler.client = client
         result = handler.process(request_mock, response_mock, {})
 
         site_adapter_mock.authenticate.assert_called_with(request_mock, {},
-                                                          scopes)
+                                                          scopes, client)
         token_generator_mock.create_access_token_data.assert_called_with(ResourceOwnerGrant.grant_type)
         access_token, = access_token_store_mock.save_token.call_args[0]
         self.assertTrue(isinstance(access_token, AccessToken))