Skip to content

Commit c113ac1

Browse files
committed
Fix TwitterMixin on Python 3.
Also add tests, and add get_auth_http_client method to all auth mixins. Closes tornadoweb#634. Conflicts: tornado/test/auth_test.py website/sphinx/releases/next.rst
1 parent 18a66be commit c113ac1

File tree

2 files changed

+110
-14
lines changed

2 files changed

+110
-14
lines changed

tornado/auth.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def get_authenticated_user(self, callback, http_client=None):
9595
args["openid.mode"] = u"check_authentication"
9696
url = self._OPENID_ENDPOINT
9797
if http_client is None:
98-
http_client = httpclient.AsyncHTTPClient()
98+
http_client = self.get_auth_http_client()
9999
http_client.fetch(url, self.async_callback(
100100
self._on_authentication_verified, callback),
101101
method="POST", body=urllib.urlencode(args))
@@ -208,6 +208,14 @@ def get_ax_arg(uri):
208208
user["claimed_id"] = claimed_id
209209
callback(user)
210210

211+
def get_auth_http_client(self):
212+
"""Returns the AsyncHTTPClient instance to be used for auth requests.
213+
214+
May be overridden by subclasses to use an http client other than
215+
the default.
216+
"""
217+
return httpclient.AsyncHTTPClient()
218+
211219

212220
class OAuthMixin(object):
213221
"""Abstract implementation of OAuth.
@@ -232,7 +240,7 @@ def authorize_redirect(self, callback_uri=None, extra_params=None,
232240
if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
233241
raise Exception("This service does not support oauth_callback")
234242
if http_client is None:
235-
http_client = httpclient.AsyncHTTPClient()
243+
http_client = self.get_auth_http_client()
236244
if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
237245
http_client.fetch(
238246
self._oauth_request_token_url(callback_uri=callback_uri,
@@ -277,7 +285,7 @@ def get_authenticated_user(self, callback, http_client=None):
277285
if oauth_verifier:
278286
token["verifier"] = oauth_verifier
279287
if http_client is None:
280-
http_client = httpclient.AsyncHTTPClient()
288+
http_client = self.get_auth_http_client()
281289
http_client.fetch(self._oauth_access_token_url(token),
282290
self.async_callback(self._on_access_token, callback))
283291

@@ -394,6 +402,14 @@ def _oauth_request_parameters(self, url, access_token, parameters={},
394402
base_args["oauth_signature"] = signature
395403
return base_args
396404

405+
def get_auth_http_client(self):
406+
"""Returns the AsyncHTTPClient instance to be used for auth requests.
407+
408+
May be overridden by subclasses to use an http client other than
409+
the default.
410+
"""
411+
return httpclient.AsyncHTTPClient()
412+
397413

398414
class OAuth2Mixin(object):
399415
"""Abstract implementation of OAuth v 2."""
@@ -471,14 +487,15 @@ def _on_auth(self, user):
471487
_OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
472488
_OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
473489
_OAUTH_NO_CALLBACKS = False
490+
_TWITTER_BASE_URL = "http://api.twitter.com/1"
474491

475492
def authenticate_redirect(self, callback_uri=None):
476493
"""Just like authorize_redirect(), but auto-redirects if authorized.
477494
478495
This is generally the right interface to use if you are using
479496
Twitter for single-sign on.
480497
"""
481-
http = httpclient.AsyncHTTPClient()
498+
http = self.get_auth_http_client()
482499
http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback(
483500
self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
484501

@@ -525,7 +542,7 @@ def _on_post(self, new_entry):
525542
# usual pattern: http://search.twitter.com/search.json
526543
url = path
527544
else:
528-
url = "http://api.twitter.com/1" + path + ".json"
545+
url = self._TWITTER_BASE_URL + path + ".json"
529546
# Add the OAuth resource request signature if we have credentials
530547
if access_token:
531548
all_args = {}
@@ -538,7 +555,7 @@ def _on_post(self, new_entry):
538555
if args:
539556
url += "?" + urllib.urlencode(args)
540557
callback = self.async_callback(self._on_twitter_request, callback)
541-
http = httpclient.AsyncHTTPClient()
558+
http = self.get_auth_http_client()
542559
if post_args is not None:
543560
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
544561
callback=callback)
@@ -563,7 +580,7 @@ def _oauth_consumer_token(self):
563580
def _oauth_get_user(self, access_token, callback):
564581
callback = self.async_callback(self._parse_user_response, callback)
565582
self.twitter_request(
566-
"/users/show/" + access_token["screen_name"],
583+
"/users/show/" + escape.native_str(access_token[b("screen_name")]),
567584
access_token=access_token, callback=callback)
568585

569586
def _parse_user_response(self, callback, user):
@@ -660,7 +677,7 @@ def _on_post(self, new_entry):
660677
if args:
661678
url += "?" + urllib.urlencode(args)
662679
callback = self.async_callback(self._on_friendfeed_request, callback)
663-
http = httpclient.AsyncHTTPClient()
680+
http = self.get_auth_http_client()
664681
if post_args is not None:
665682
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
666683
callback=callback)
@@ -751,7 +768,7 @@ def get_authenticated_user(self, callback):
751768
break
752769
token = self.get_argument("openid." + oauth_ns + ".request_token", "")
753770
if token:
754-
http = httpclient.AsyncHTTPClient()
771+
http = self.get_auth_http_client()
755772
token = dict(key=token, secret="")
756773
http.fetch(self._oauth_access_token_url(token),
757774
self.async_callback(self._on_access_token, callback))
@@ -907,7 +924,7 @@ def _on_stream(self, stream):
907924
args["sig"] = self._signature(args)
908925
url = "http://api.facebook.com/restserver.php?" + \
909926
urllib.urlencode(args)
910-
http = httpclient.AsyncHTTPClient()
927+
http = self.get_auth_http_client()
911928
http.fetch(url, callback=self.async_callback(
912929
self._parse_response, callback))
913930

@@ -953,6 +970,14 @@ def _signature(self, args):
953970
body = body.encode("utf-8")
954971
return hashlib.md5(body).hexdigest()
955972

973+
def get_auth_http_client(self):
974+
"""Returns the AsyncHTTPClient instance to be used for auth requests.
975+
976+
May be overridden by subclasses to use an http client other than
977+
the default.
978+
"""
979+
return httpclient.AsyncHTTPClient()
980+
956981

957982
class FacebookGraphMixin(OAuth2Mixin):
958983
"""Facebook authentication using the new Graph API and OAuth2."""
@@ -987,7 +1012,7 @@ def _on_login(self, user):
9871012
self.finish()
9881013
9891014
"""
990-
http = httpclient.AsyncHTTPClient()
1015+
http = self.get_auth_http_client()
9911016
args = {
9921017
"redirect_uri": redirect_uri,
9931018
"code": code,
@@ -1081,7 +1106,7 @@ def _on_post(self, new_entry):
10811106
if all_args:
10821107
url += "?" + urllib.urlencode(all_args)
10831108
callback = self.async_callback(self._on_facebook_request, callback)
1084-
http = httpclient.AsyncHTTPClient()
1109+
http = self.get_auth_http_client()
10851110
if post_args is not None:
10861111
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
10871112
callback=callback)
@@ -1096,6 +1121,14 @@ def _on_facebook_request(self, callback, response):
10961121
return
10971122
callback(escape.json_decode(response.body))
10981123

1124+
def get_auth_http_client(self):
1125+
"""Returns the AsyncHTTPClient instance to be used for auth requests.
1126+
1127+
May be overridden by subclasses to use an http client other than
1128+
the default.
1129+
"""
1130+
return httpclient.AsyncHTTPClient()
1131+
10991132

11001133
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
11011134
"""Calculates the HMAC-SHA1 OAuth signature for the given request.

tornado/test/auth_test.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
from __future__ import absolute_import, division, with_statement
8-
from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin
8+
from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin
99
from tornado.escape import json_decode
1010
from tornado.testing import AsyncHTTPTestCase, LogTrapTestCase
1111
from tornado.util import b
@@ -101,6 +101,37 @@ def get(self):
101101
self.authorize_redirect()
102102

103103

104+
class TwitterClientLoginHandler(RequestHandler, TwitterMixin):
105+
def initialize(self, test):
106+
self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token')
107+
self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/twitter/server/access_token')
108+
self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth1/server/authorize')
109+
self._TWITTER_BASE_URL = test.get_url('/twitter/api')
110+
111+
@asynchronous
112+
def get(self):
113+
if self.get_argument("oauth_token", None):
114+
self.get_authenticated_user(self.on_user)
115+
return
116+
self.authorize_redirect()
117+
118+
def on_user(self, user):
119+
if user is None:
120+
raise Exception("user is None")
121+
self.finish(user)
122+
123+
def get_auth_http_client(self):
124+
return self.settings['http_client']
125+
126+
127+
class TwitterServerAccessTokenHandler(RequestHandler):
128+
def get(self):
129+
self.write('oauth_token=hjkl&oauth_token_secret=vbnm&screen_name=foo')
130+
131+
class TwitterServerShowUserHandler(RequestHandler):
132+
def get(self, screen_name):
133+
self.write(dict(screen_name=screen_name, name=screen_name.capitalize()))
134+
104135
class AuthTest(AsyncHTTPTestCase, LogTrapTestCase):
105136
def get_app(self):
106137
return Application(
@@ -119,12 +150,19 @@ def get_app(self):
119150
dict(version='1.0a')),
120151
('/oauth2/client/login', OAuth2ClientLoginHandler, dict(test=self)),
121152

153+
('/twitter/client/login', TwitterClientLoginHandler, dict(test=self)),
154+
122155
# simulated servers
123156
('/openid/server/authenticate', OpenIdServerAuthenticateHandler),
124157
('/oauth1/server/request_token', OAuth1ServerRequestTokenHandler),
125158
('/oauth1/server/access_token', OAuth1ServerAccessTokenHandler),
159+
160+
('/twitter/server/access_token', TwitterServerAccessTokenHandler),
161+
(r'/twitter/api/users/show/(.*)\.json', TwitterServerShowUserHandler),
126162
],
127-
http_client=self.http_client)
163+
http_client=self.http_client,
164+
twitter_consumer_key='test_twitter_consumer_key',
165+
twitter_consumer_secret='test_twitter_consumer_secret')
128166

129167
def test_openid_redirect(self):
130168
response = self.fetch('/openid/client/login', follow_redirects=False)
@@ -198,3 +236,28 @@ def test_oauth2_redirect(self):
198236
response = self.fetch('/oauth2/client/login', follow_redirects=False)
199237
self.assertEqual(response.code, 302)
200238
self.assertTrue('/oauth2/server/authorize?' in response.headers['Location'])
239+
240+
def test_twitter_redirect(self):
241+
# Same as test_oauth10a_redirect
242+
response = self.fetch('/twitter/client/login', follow_redirects=False)
243+
self.assertEqual(response.code, 302)
244+
self.assertTrue(response.headers['Location'].endswith(
245+
'/oauth1/server/authorize?oauth_token=zxcv'))
246+
# the cookie is base64('zxcv')|base64('1234')
247+
self.assertTrue(
248+
'_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'],
249+
response.headers['Set-Cookie'])
250+
251+
def test_twitter_get_user(self):
252+
response = self.fetch(
253+
'/twitter/client/login?oauth_token=zxcv',
254+
headers={'Cookie': '_oauth_request_token=enhjdg==|MTIzNA=='})
255+
response.rethrow()
256+
parsed = json_decode(response.body)
257+
self.assertEqual(parsed,
258+
{u'access_token': {u'key': u'hjkl',
259+
u'screen_name': u'foo',
260+
u'secret': u'vbnm'},
261+
u'name': u'Foo',
262+
u'screen_name': u'foo',
263+
u'username': u'foo'})

0 commit comments

Comments
 (0)