]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Introduce OAuth2Mixin.oauth2_request.
authorBen Darnell <ben@bendarnell.com>
Sun, 28 Jun 2015 20:41:46 +0000 (16:41 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 28 Jun 2015 20:41:46 +0000 (16:41 -0400)
This method can serve as a general replacement for methods like
FacebookGraphMixin.facebook_request.

Update docs for GoogleOAuth2Mixin.get_authenticated_user to indicate
that only the tokens are returned, not other user information.

Add some basic tests for GoogleOAuth2Mixin.

This commit incorporates code from FanFani4 in #1212 and
kippandrew in #1454.

tornado/auth.py
tornado/test/auth_test.py

index 800b10afe455088e49326f0aaf8ab432c8842ed0..ebf0ecdd74f2aaa1856f6e4e0e88590e16b3a1e8 100644 (file)
@@ -621,6 +621,72 @@ class OAuth2Mixin(object):
             args.update(extra_params)
         return url_concat(url, args)
 
+    @_auth_return_future
+    def oauth2_request(self, url, callback, access_token=None,
+                       post_args=None, **args):
+        """Fetches the given URL auth an OAuth2 access token.
+
+        If the request is a POST, ``post_args`` should be provided. Query
+        string arguments should be given as keyword arguments.
+
+        Example usage:
+
+        ..testcode::
+
+            class MainHandler(tornado.web.RequestHandler,
+                              tornado.auth.FacebookGraphMixin):
+                @tornado.web.authenticated
+                @tornado.gen.coroutine
+                def get(self):
+                    new_entry = yield self.oauth2_request(
+                        "https://graph.facebook.com/me/feed",
+                        post_args={"message": "I am posting from my Tornado application!"},
+                        access_token=self.current_user["access_token"])
+
+                    if not new_entry:
+                        # Call failed; perhaps missing permission?
+                        yield self.authorize_redirect()
+                        return
+                    self.finish("Posted a message!")
+
+        .. testoutput::
+           :hide:
+
+        .. versionadded:: 4.3
+        """
+        all_args = {}
+        if access_token:
+            all_args["access_token"] = access_token
+            all_args.update(args)
+
+        if all_args:
+            url += "?" + urllib_parse.urlencode(all_args)
+        callback = functools.partial(self._on_facebook_request, callback)
+        http = self.get_auth_http_client()
+        if post_args is not None:
+            http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
+                       callback=callback)
+        else:
+            http.fetch(url, callback=callback)
+
+    def _on_oauth2_request(self, future, response):
+        if response.error:
+            future.set_exception(AuthError("Error response %s fetching %s" %
+                                           (response.error, response.request.url)))
+            return
+
+        future.set_result(escape.json_decode(response.body))
+
+    def get_auth_http_client(self):
+        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
+
+        May be overridden by subclasses to use an HTTP client other than
+        the default.
+
+        .. versionadded:: 4.3
+        """
+        return httpclient.AsyncHTTPClient()
+
 
 class TwitterMixin(OAuthMixin):
     """Twitter OAuth authentication.
@@ -796,7 +862,15 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
 
     @_auth_return_future
     def get_authenticated_user(self, redirect_uri, code, callback):
-        """Handles the login for the Google user, returning a user object.
+        """Handles the login for the Google user, returning an access token.
+
+        The result is a dictionary containing an ``access_token`` field
+        ([among others](https://developers.google.com/identity/protocols/OAuth2WebServer#handlingtheresponse)).
+        Unlike other ``get_authenticated_user`` methods in this package,
+        this method does not return any additional information about the user.
+        The returned access token can be used with `OAuth2Mixin.oauth2_request`
+        to request additional information (perhaps from
+        ``https://www.googleapis.com/oauth2/v2/userinfo``)
 
         Example usage:
 
@@ -845,14 +919,6 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
         args = escape.json_decode(response.body)
         future.set_result(args)
 
-    def get_auth_http_client(self):
-        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
-
-        May be overridden by subclasses to use an HTTP client other than
-        the default.
-        """
-        return httpclient.AsyncHTTPClient()
-
 
 class FacebookGraphMixin(OAuth2Mixin):
     """Facebook authentication using the new Graph API and OAuth2."""
@@ -983,40 +1049,16 @@ class FacebookGraphMixin(OAuth2Mixin):
         The given path is relative to ``self._FACEBOOK_BASE_URL``,
         by default "https://graph.facebook.com".
 
+        This method is a wrapper around `OAuth2Mixin.oauth2_request`;
+        the only difference is that this method takes a relative path,
+        while ``oauth2_request`` takes a complete url.
+
         .. versionchanged:: 3.1
            Added the ability to override ``self._FACEBOOK_BASE_URL``.
         """
         url = self._FACEBOOK_BASE_URL + path
-        all_args = {}
-        if access_token:
-            all_args["access_token"] = access_token
-            all_args.update(args)
-
-        if all_args:
-            url += "?" + urllib_parse.urlencode(all_args)
-        callback = functools.partial(self._on_facebook_request, callback)
-        http = self.get_auth_http_client()
-        if post_args is not None:
-            http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
-                       callback=callback)
-        else:
-            http.fetch(url, callback=callback)
-
-    def _on_facebook_request(self, future, response):
-        if response.error:
-            future.set_exception(AuthError("Error response %s fetching %s" %
-                                           (response.error, response.request.url)))
-            return
-
-        future.set_result(escape.json_decode(response.body))
-
-    def get_auth_http_client(self):
-        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
-
-        May be overridden by subclasses to use an HTTP client other than
-        the default.
-        """
-        return httpclient.AsyncHTTPClient()
+        return self.oauth2_request(url, callback, access_token,
+                                   post_args, **args)
 
 
 def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
index 541ecf16f3888993d2a54f164a85fd5450bd71a1..fee79779952018e7ee7a44950a25482d761304c4 100644 (file)
@@ -5,10 +5,11 @@
 
 
 from __future__ import absolute_import, division, print_function, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError, GoogleOAuth2Mixin
 from tornado.concurrent import Future
 from tornado.escape import json_decode
 from tornado import gen
+from tornado.httputil import url_concat
 from tornado.log import gen_log
 from tornado.testing import AsyncHTTPTestCase, ExpectLog
 from tornado.util import u
@@ -413,3 +414,70 @@ class AuthTest(AsyncHTTPTestCase):
         response = self.fetch('/twitter/client/show_user_future?name=error')
         self.assertEqual(response.code, 500)
         self.assertIn(b'Error response HTTP 500', response.body)
+
+
+class GoogleLoginHandler(RequestHandler, GoogleOAuth2Mixin):
+    def initialize(self, test):
+        self._OAUTH_REDIRECT_URI = test.get_url('/client/login')
+        self._OAUTH_AUTHORIZE_URL = test.get_url('/google/oauth2/authorize')
+        self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/google/oauth2/token')
+
+    @gen.coroutine
+    def get(self):
+        code = self.get_argument('code', None)
+        if code is not None:
+            # retrieve authenticate google user
+            user = yield self.get_authenticated_user(self._OAUTH_REDIRECT_URI,
+                                                     code)
+            # return the user as json
+            self.write(user)
+        else:
+            yield self.authorize_redirect(
+                redirect_uri=self._OAUTH_REDIRECT_URI,
+                client_id=self.settings['google_oauth']['key'],
+                client_secret=self.settings['google_oauth']['secret'],
+                scope=['profile', 'email'],
+                response_type='code',
+                extra_params={'prompt': 'select_account'})
+
+
+class GoogleOAuth2AuthorizeHandler(RequestHandler):
+    def get(self):
+        # issue a fake auth code and redirect to redirect_uri
+        code = 'fake-authorization-code'
+        self.redirect(url_concat(self.get_argument('redirect_uri'),
+                                 dict(code=code)))
+
+
+class GoogleOAuth2TokenHandler(RequestHandler):
+    def post(self):
+        assert self.get_argument('code') == 'fake-authorization-code'
+        # issue a fake token
+        self.finish({
+            'access_token': 'fake-access-token',
+            'expires_in': 'never-expires'
+        })
+
+
+class GoogleOAuth2Test(AsyncHTTPTestCase):
+    def get_app(self):
+        return Application(
+            [
+                # test endpoints
+                ('/client/login', GoogleLoginHandler, dict(test=self)),
+
+                # simulated google authorization server endpoints
+                ('/google/oauth2/authorize', GoogleOAuth2AuthorizeHandler),
+                ('/google/oauth2/token', GoogleOAuth2TokenHandler),
+            ],
+            google_oauth={
+                "key": 'fake_google_client_id',
+                "secret": 'fake_google_client_secret'
+            })
+
+    def test_google_login(self):
+        response = self.fetch('/client/login')
+        self.assertDictEqual({
+            u('access_token'): u('fake-access-token'),
+            u('expires_in'): u('never-expires'),
+        }, json_decode(response.body))