Repository: tornadoweb/tornado

Issue:
FacebookGraphMixin failing
I had to modify `_on_access_token` so that it did not attempt to make a `facebook_request` to `/me`. With the code as it stands, I get this error:

`TypeError: 'Future' object is not callable`

However, removing that extra request fixes this, and I can use the access_token in the next part of my auth pipeline.



Discussion:
Can you share the full stack trace? Where is that exception raised? Which version of Tornado? How are you calling whichever method you are calling?

Tornado version 4.2.1
It's being called in the web.RequestHandler `get()` method using the `FacebookGraphMixin`'s implementation of `get_authenticated_user()`.

full trace:

```
[E 150928 16:53:09 concurrent:303] Exception in callback <functools.partial object at 0x7f22b6999a48> for <tornado.concurrent.Future object at 0x7f22b64ddbd0>
    Traceback (most recent call last):
      File "/usr/local/lib/python2.7/dist-packages/tornado/concurrent.py", line 300, in _set_done
        cb(self)
      File "/usr/local/lib/python2.7/dist-packages/tornado/auth.py", line 114, in _auth_future_to_callback
        callback(result)
    TypeError: 'Future' object is not callable
```

And how are you calling `get_authenticated_user()`? It looks like something funny may be going on there.

```
if self.get_argument('code', None):
            access = yield self.get_authenticated_user(
                    redirect_uri= 'https://iamadatapoint.com/auth/facebook',
                    client_id=self.application.settings['facebook_oauth']['key'],
                    client_secret=self.application.settings['facebook_oauth']['secret'],
                    code=self.get_argument('code'))
```

I dug into the source code, but haven't done much to mess with the `facebook_request` method, which is what I'm doing now

Just put everything back to how it was, and it's hanging on the Future again, as in the stack trace above.

Hmm, I actually see a way it might be broken in the way you describe in the `master` branch, but it looks like it was working in 4.2.1. I'll have to put together a test case.

OK so the error is definitely happening somewhere in `self.oauth2_request`, which is called by `facebook_request` 

If the `oauth2_request` method exists you're definitely on the `master` branch instead of the 4.2.1 release. I see the problem there and am working on a test and fix.

I have whatever `pip install --upgrade tornado` gives you

On Mon, Sep 28, 2015 at 8:23 PM, Ben Darnell notifications@github.com
wrote:

> If the oauth2_request method exists you're definitely on the master
> branch instead of the 4.2.1 release. I see the problem there and am working
> on a test and fix.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/tornadoweb/tornado/issues/1532#issuecomment-143925511
> .

Hmm. That should only be 4.2.1, and I don't see `oauth2_request` there:

```
[bdarnell@rowlf ~]$ virtualenv /tmp/testenv
Using base prefix '/opt/local/Library/Frameworks/Python.framework/Versions/3.4'
New python executable in /tmp/testenv/bin/python3.4
Also creating executable in /tmp/testenv/bin/python
Installing setuptools, pip, wheel...done.
[bdarnell@rowlf ~]$ /tmp/testenv/bin/pip install tornado
Collecting tornado
  Using cached tornado-4.2.1.tar.gz
Building wheels for collected packages: tornado
  Running setup.py bdist_wheel for tornado
  Stored in directory: /Users/bdarnell/Library/Caches/pip/wheels/e4/0e/f8/2c7f5423e2dcad6a71c6f04e859f35cb3cf962da96f22a74c3
Successfully built tornado
Installing collected packages: tornado
Successfully installed tornado-4.2.1
[bdarnell@rowlf ~]$ ag oauth2_request /tmp/testenv/lib/python3.4/site-packages/tornado
```


Resolving patch:
diff --git a/tornado/auth.py b/tornado/auth.py
--- a/tornado/auth.py
+++ b/tornado/auth.py
@@ -75,7 +75,7 @@ def get(self):
 import time
 import uuid
 
-from tornado.concurrent import TracebackFuture, return_future
+from tornado.concurrent import TracebackFuture, return_future, chain_future
 from tornado import gen
 from tornado import httpclient
 from tornado import escape
@@ -985,7 +985,7 @@ def _on_access_token(self, redirect_uri, client_id, client_secret,
             future.set_exception(AuthError('Facebook auth error: %s' % str(response)))
             return
 
-        args = escape.parse_qs_bytes(escape.native_str(response.body))
+        args = urlparse.parse_qs(escape.native_str(response.body))
         session = {
             "access_token": args["access_token"][-1],
             "expires": args.get("expires")
@@ -1062,8 +1062,13 @@ def get(self):
            Added the ability to override ``self._FACEBOOK_BASE_URL``.
         """
         url = self._FACEBOOK_BASE_URL + path
-        return self.oauth2_request(url, callback, access_token,
-                                   post_args, **args)
+        # Thanks to the _auth_return_future decorator, our "callback"
+        # argument is a Future, which we cannot pass as a callback to
+        # oauth2_request. Instead, have oauth2_request return a
+        # future and chain them together.
+        oauth_future = self.oauth2_request(url, access_token=access_token,
+                                           post_args=post_args, **args)
+        chain_future(oauth_future, callback)
 
 
 def _oauth_signature(consumer_token, method, url, parameters={}, token=None):


Test patch:
diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py
--- a/tornado/test/auth_test.py
+++ b/tornado/test/auth_test.py
@@ -5,7 +5,7 @@
 
 
 from __future__ import absolute_import, division, print_function, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError, GoogleOAuth2Mixin
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError, GoogleOAuth2Mixin, FacebookGraphMixin
 from tornado.concurrent import Future
 from tornado.escape import json_decode
 from tornado import gen
@@ -126,6 +126,38 @@ def get(self):
         assert res.done()
 
 
+class FacebookClientLoginHandler(RequestHandler, FacebookGraphMixin):
+    def initialize(self, test):
+        self._OAUTH_AUTHORIZE_URL = test.get_url('/facebook/server/authorize')
+        self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/facebook/server/access_token')
+        self._FACEBOOK_BASE_URL = test.get_url('/facebook/server')
+
+    @gen.coroutine
+    def get(self):
+        if self.get_argument("code", None):
+            user = yield self.get_authenticated_user(
+                redirect_uri=self.request.full_url(),
+                client_id=self.settings["facebook_api_key"],
+                client_secret=self.settings["facebook_secret"],
+                code=self.get_argument("code"))
+            self.write(user)
+        else:
+                yield self.authorize_redirect(
+                    redirect_uri=self.request.full_url(),
+                    client_id=self.settings["facebook_api_key"],
+                    extra_params={"scope": "read_stream,offline_access"})
+
+
+class FacebookServerAccessTokenHandler(RequestHandler):
+    def get(self):
+        self.write('access_token=asdf')
+
+
+class FacebookServerMeHandler(RequestHandler):
+    def get(self):
+        self.write('{}')
+
+
 class TwitterClientHandler(RequestHandler, TwitterMixin):
     def initialize(self, test):
         self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token')
@@ -260,6 +292,8 @@ def get_app(self):
                  dict(version='1.0a')),
                 ('/oauth2/client/login', OAuth2ClientLoginHandler, dict(test=self)),
 
+                ('/facebook/client/login', FacebookClientLoginHandler, dict(test=self)),
+
                 ('/twitter/client/login', TwitterClientLoginHandler, dict(test=self)),
                 ('/twitter/client/login_gen_engine', TwitterClientLoginGenEngineHandler, dict(test=self)),
                 ('/twitter/client/login_gen_coroutine', TwitterClientLoginGenCoroutineHandler, dict(test=self)),
@@ -271,13 +305,17 @@ def get_app(self):
                 ('/oauth1/server/request_token', OAuth1ServerRequestTokenHandler),
                 ('/oauth1/server/access_token', OAuth1ServerAccessTokenHandler),
 
+                ('/facebook/server/access_token', FacebookServerAccessTokenHandler),
+                ('/facebook/server/me', FacebookServerMeHandler),
                 ('/twitter/server/access_token', TwitterServerAccessTokenHandler),
                 (r'/twitter/api/users/show/(.*)\.json', TwitterServerShowUserHandler),
                 (r'/twitter/api/account/verify_credentials\.json', TwitterServerVerifyCredentialsHandler),
             ],
             http_client=self.http_client,
             twitter_consumer_key='test_twitter_consumer_key',
-            twitter_consumer_secret='test_twitter_consumer_secret')
+            twitter_consumer_secret='test_twitter_consumer_secret',
+            facebook_api_key='test_facebook_api_key',
+            facebook_secret='test_facebook_secret')
 
     def test_openid_redirect(self):
         response = self.fetch('/openid/client/login', follow_redirects=False)
@@ -358,6 +396,13 @@ def test_oauth2_redirect(self):
         self.assertEqual(response.code, 302)
         self.assertTrue('/oauth2/server/authorize?' in response.headers['Location'])
 
+    def test_facebook_login(self):
+        response = self.fetch('/facebook/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue('/facebook/server/authorize?' in response.headers['Location'])
+        response = self.fetch('/facebook/client/login?code=1234', follow_redirects=False)
+        self.assertEqual(response.code, 200)
+
     def base_twitter_redirect(self, url):
         # Same as test_oauth10a_redirect
         response = self.fetch(url, follow_redirects=False)