Permalink
Browse files

Fix TwitterMixin on Python 3.

Also add tests, and add get_auth_http_client method to all auth mixins.

Closes #634.
  • Loading branch information...
bdarnell committed Nov 17, 2012
1 parent 5f597b5 commit e7485f858c3df3538b6334a6d0bf3d318ec9afed
Showing with 114 additions and 14 deletions.
  1. +45 −12 tornado/auth.py
  2. +65 −2 tornado/test/auth_test.py
  3. +4 −0 website/sphinx/releases/next.rst
View
@@ -95,7 +95,7 @@ def get_authenticated_user(self, callback, http_client=None):
args["openid.mode"] = u"check_authentication"
url = self._OPENID_ENDPOINT
if http_client is None:
- http_client = httpclient.AsyncHTTPClient()
+ http_client = self.get_auth_http_client()
http_client.fetch(url, self.async_callback(
self._on_authentication_verified, callback),
method="POST", body=urllib.urlencode(args))
@@ -208,6 +208,14 @@ def get_ax_arg(uri):
user["claimed_id"] = claimed_id
callback(user)
+ 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 OAuthMixin(object):
"""Abstract implementation of OAuth.
@@ -232,7 +240,7 @@ def authorize_redirect(self, callback_uri=None, extra_params=None,
if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
raise Exception("This service does not support oauth_callback")
if http_client is None:
- http_client = httpclient.AsyncHTTPClient()
+ http_client = self.get_auth_http_client()
if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
http_client.fetch(
self._oauth_request_token_url(callback_uri=callback_uri,
@@ -277,7 +285,7 @@ def get_authenticated_user(self, callback, http_client=None):
if oauth_verifier:
token["verifier"] = oauth_verifier
if http_client is None:
- http_client = httpclient.AsyncHTTPClient()
+ http_client = self.get_auth_http_client()
http_client.fetch(self._oauth_access_token_url(token),
self.async_callback(self._on_access_token, callback))
@@ -394,6 +402,14 @@ def _oauth_request_parameters(self, url, access_token, parameters={},
base_args["oauth_signature"] = signature
return base_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 OAuth2Mixin(object):
"""Abstract implementation of OAuth v 2."""
@@ -471,14 +487,15 @@ def _on_auth(self, user):
_OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
_OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
_OAUTH_NO_CALLBACKS = False
+ _TWITTER_BASE_URL = "http://api.twitter.com/1"
def authenticate_redirect(self, callback_uri=None):
"""Just like authorize_redirect(), but auto-redirects if authorized.
This is generally the right interface to use if you are using
Twitter for single-sign on.
"""
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback(
self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
@@ -525,7 +542,7 @@ def _on_post(self, new_entry):
# usual pattern: http://search.twitter.com/search.json
url = path
else:
- url = "http://api.twitter.com/1" + path + ".json"
+ url = self._TWITTER_BASE_URL + path + ".json"
# Add the OAuth resource request signature if we have credentials
if access_token:
all_args = {}
@@ -538,7 +555,7 @@ def _on_post(self, new_entry):
if args:
url += "?" + urllib.urlencode(args)
callback = self.async_callback(self._on_twitter_request, callback)
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
callback=callback)
@@ -563,7 +580,7 @@ def _oauth_consumer_token(self):
def _oauth_get_user(self, access_token, callback):
callback = self.async_callback(self._parse_user_response, callback)
self.twitter_request(
- "/users/show/" + access_token["screen_name"],
+ "/users/show/" + escape.native_str(access_token[b("screen_name")]),
access_token=access_token, callback=callback)
def _parse_user_response(self, callback, user):
@@ -660,7 +677,7 @@ def _on_post(self, new_entry):
if args:
url += "?" + urllib.urlencode(args)
callback = self.async_callback(self._on_friendfeed_request, callback)
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
callback=callback)
@@ -751,7 +768,7 @@ def get_authenticated_user(self, callback):
break
token = self.get_argument("openid." + oauth_ns + ".request_token", "")
if token:
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
token = dict(key=token, secret="")
http.fetch(self._oauth_access_token_url(token),
self.async_callback(self._on_access_token, callback))
@@ -907,7 +924,7 @@ def _on_stream(self, stream):
args["sig"] = self._signature(args)
url = "http://api.facebook.com/restserver.php?" + \
urllib.urlencode(args)
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
http.fetch(url, callback=self.async_callback(
self._parse_response, callback))
@@ -953,6 +970,14 @@ def _signature(self, args):
body = body.encode("utf-8")
return hashlib.md5(body).hexdigest()
+ 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."""
@@ -987,7 +1012,7 @@ def _on_login(self, user):
self.finish()
"""
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
args = {
"redirect_uri": redirect_uri,
"code": code,
@@ -1081,7 +1106,7 @@ def _on_post(self, new_entry):
if all_args:
url += "?" + urllib.urlencode(all_args)
callback = self.async_callback(self._on_facebook_request, callback)
- http = httpclient.AsyncHTTPClient()
+ http = self.get_auth_http_client()
if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
callback=callback)
@@ -1096,6 +1121,14 @@ def _on_facebook_request(self, callback, response):
return
callback(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()
+
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
"""Calculates the HMAC-SHA1 OAuth signature for the given request.
View
@@ -5,7 +5,7 @@
from __future__ import absolute_import, division, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin
from tornado.escape import json_decode
from tornado.testing import AsyncHTTPTestCase
from tornado.util import b
@@ -101,6 +101,37 @@ def get(self):
self.authorize_redirect()
+class TwitterClientLoginHandler(RequestHandler, TwitterMixin):
+ def initialize(self, test):
+ self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token')
+ self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/twitter/server/access_token')
+ self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth1/server/authorize')
+ self._TWITTER_BASE_URL = test.get_url('/twitter/api')
+
+ @asynchronous
+ def get(self):
+ if self.get_argument("oauth_token", None):
+ self.get_authenticated_user(self.on_user)
+ return
+ self.authorize_redirect()
+
+ def on_user(self, user):
+ if user is None:
+ raise Exception("user is None")
+ self.finish(user)
+
+ def get_auth_http_client(self):
+ return self.settings['http_client']
+
+
+class TwitterServerAccessTokenHandler(RequestHandler):
+ def get(self):
+ self.write('oauth_token=hjkl&oauth_token_secret=vbnm&screen_name=foo')
+
+class TwitterServerShowUserHandler(RequestHandler):
+ def get(self, screen_name):
+ self.write(dict(screen_name=screen_name, name=screen_name.capitalize()))
+
class AuthTest(AsyncHTTPTestCase):
def get_app(self):
return Application(
@@ -119,12 +150,19 @@ def get_app(self):
dict(version='1.0a')),
('/oauth2/client/login', OAuth2ClientLoginHandler, dict(test=self)),
+ ('/twitter/client/login', TwitterClientLoginHandler, dict(test=self)),
+
# simulated servers
('/openid/server/authenticate', OpenIdServerAuthenticateHandler),
('/oauth1/server/request_token', OAuth1ServerRequestTokenHandler),
('/oauth1/server/access_token', OAuth1ServerAccessTokenHandler),
+
+ ('/twitter/server/access_token', TwitterServerAccessTokenHandler),
+ (r'/twitter/api/users/show/(.*)\.json', TwitterServerShowUserHandler),
],
- http_client=self.http_client)
+ http_client=self.http_client,
+ twitter_consumer_key='test_twitter_consumer_key',
+ twitter_consumer_secret='test_twitter_consumer_secret')
def test_openid_redirect(self):
response = self.fetch('/openid/client/login', follow_redirects=False)
@@ -198,3 +236,28 @@ def test_oauth2_redirect(self):
response = self.fetch('/oauth2/client/login', follow_redirects=False)
self.assertEqual(response.code, 302)
self.assertTrue('/oauth2/server/authorize?' in response.headers['Location'])
+
+ def test_twitter_redirect(self):
+ # Same as test_oauth10a_redirect
+ response = self.fetch('/twitter/client/login', follow_redirects=False)
+ self.assertEqual(response.code, 302)
+ self.assertTrue(response.headers['Location'].endswith(
+ '/oauth1/server/authorize?oauth_token=zxcv'))
+ # the cookie is base64('zxcv')|base64('1234')
+ self.assertTrue(
+ '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'],
+ response.headers['Set-Cookie'])
+
+ def test_twitter_get_user(self):
+ response = self.fetch(
+ '/twitter/client/login?oauth_token=zxcv',
+ headers={'Cookie': '_oauth_request_token=enhjdg==|MTIzNA=='})
+ response.rethrow()
+ parsed = json_decode(response.body)
+ self.assertEqual(parsed,
+ {u'access_token': {u'key': u'hjkl',
+ u'screen_name': u'foo',
+ u'secret': u'vbnm'},
+ u'name': u'Foo',
+ u'screen_name': u'foo',
+ u'username': u'foo'})
@@ -159,3 +159,7 @@ In progress
* Fixed a bug with `IOStream.read_until_close` with a ``streaming_callback``,
which would cause some data to be passed to the final callback instead
of the streaming callback.
+* The `tornado.auth` mixin classes now define a method
+ ``get_auth_http_client``, which can be overridden to use a non-default
+ `AsyncHTTPClient` instance (e.g. to use a different `IOLoop`)
+* `tornado.auth.TwitterMixin` now works on Python 3.

0 comments on commit e7485f8

Please sign in to comment.