3
# Copyright 2009 Facebook
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
17
"""Implementations of various third-party authentication schemes.
19
All the classes in this file are class Mixins designed to be used with
20
web.py RequestHandler classes. The primary methods for each service are
21
authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
22
The former should be called to redirect the user to, e.g., the OpenID
23
authentication page on the third party service, and the latter should
24
be called upon return to get the user data from the data returned by
25
the third party service.
27
They all take slightly different arguments due to the fact all these
28
services implement authentication and authorization slightly differently.
29
See the individual service classes below for complete documentation.
31
Example usage for Google OpenID:
33
class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
34
@tornado.web.asynchronous
36
if self.get_argument("openid.mode", None):
37
self.get_authenticated_user(self.async_callback(self._on_auth))
39
self.authenticate_redirect()
41
def _on_auth(self, user):
43
raise tornado.web.HTTPError(500, "Google auth failed")
44
# Save the user with, e.g., set_secure_cookie()
61
_log = logging.getLogger("tornado.auth")
63
class OpenIdMixin(object):
64
"""Abstract implementation of OpenID and Attribute Exchange.
66
See GoogleMixin below for example implementations.
68
def authenticate_redirect(self, callback_uri=None,
69
ax_attrs=["name","email","language","username"]):
70
"""Returns the authentication URL for this service.
72
After authentication, the service will redirect back to the given
75
We request the given attributes for the authenticated user by
76
default (name, email, language, and username). If you don't need
77
all those attributes for your app, you can request fewer with
78
the ax_attrs keyword argument.
80
callback_uri = callback_uri or self.request.path
81
args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
82
self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
84
def get_authenticated_user(self, callback):
85
"""Fetches the authenticated user data upon redirect.
87
This method should be called by the handler that receives the
88
redirect from the authenticate_redirect() or authorize_redirect()
91
# Verify the OpenID response via direct request to the OP
92
args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
93
args["openid.mode"] = u"check_authentication"
94
url = self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)
95
http = httpclient.AsyncHTTPClient()
96
http.fetch(url, self.async_callback(
97
self._on_authentication_verified, callback))
99
def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
100
url = urlparse.urljoin(self.request.full_url(), callback_uri)
102
"openid.ns": "http://specs.openid.net/auth/2.0",
104
"http://specs.openid.net/auth/2.0/identifier_select",
106
"http://specs.openid.net/auth/2.0/identifier_select",
107
"openid.return_to": url,
108
"openid.realm": "http://" + self.request.host + "/",
109
"openid.mode": "checkid_setup",
113
"openid.ns.ax": "http://openid.net/srv/ax/1.0",
114
"openid.ax.mode": "fetch_request",
116
ax_attrs = set(ax_attrs)
118
if "name" in ax_attrs:
119
ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
120
required += ["firstname", "fullname", "lastname"]
122
"openid.ax.type.firstname":
123
"http://axschema.org/namePerson/first",
124
"openid.ax.type.fullname":
125
"http://axschema.org/namePerson",
126
"openid.ax.type.lastname":
127
"http://axschema.org/namePerson/last",
130
"email": "http://axschema.org/contact/email",
131
"language": "http://axschema.org/pref/language",
132
"username": "http://axschema.org/namePerson/friendly",
134
for name in ax_attrs:
135
args["openid.ax.type." + name] = known_attrs[name]
136
required.append(name)
137
args["openid.ax.required"] = ",".join(required)
141
"http://specs.openid.net/extensions/oauth/1.0",
142
"openid.oauth.consumer": self.request.host.split(":")[0],
143
"openid.oauth.scope": oauth_scope,
147
def _on_authentication_verified(self, callback, response):
148
if response.error or u"is_valid:true" not in response.body:
149
_log.warning("Invalid OpenID response: %s", response.error or
154
# Make sure we got back at least an email from attribute exchange
156
for name, values in self.request.arguments.iteritems():
157
if name.startswith("openid.ns.") and \
158
values[-1] == u"http://openid.net/srv/ax/1.0":
162
if not ax_ns: return u""
163
prefix = "openid." + ax_ns + ".type."
165
for name, values in self.request.arguments.iteritems():
166
if values[-1] == uri and name.startswith(prefix):
167
part = name[len(prefix):]
168
ax_name = "openid." + ax_ns + ".value." + part
170
if not ax_name: return u""
171
return self.get_argument(ax_name, u"")
173
email = get_ax_arg("http://axschema.org/contact/email")
174
name = get_ax_arg("http://axschema.org/namePerson")
175
first_name = get_ax_arg("http://axschema.org/namePerson/first")
176
last_name = get_ax_arg("http://axschema.org/namePerson/last")
177
username = get_ax_arg("http://axschema.org/namePerson/friendly")
178
locale = get_ax_arg("http://axschema.org/pref/language").lower()
182
user["first_name"] = first_name
183
name_parts.append(first_name)
185
user["last_name"] = last_name
186
name_parts.append(last_name)
190
user["name"] = u" ".join(name_parts)
192
user["name"] = email.split("@")[0]
193
if email: user["email"] = email
194
if locale: user["locale"] = locale
195
if username: user["username"] = username
199
class OAuthMixin(object):
200
"""Abstract implementation of OAuth.
202
See TwitterMixin and FriendFeedMixin below for example implementations.
204
def authorize_redirect(self, callback_uri=None):
205
"""Redirects the user to obtain OAuth authorization for this service.
207
Twitter and FriendFeed both require that you register a Callback
208
URL with your application. You should call this method to log the
209
user in, and then call get_authenticated_user() in the handler
210
you registered as your Callback URL to complete the authorization
213
This method sets a cookie called _oauth_request_token which is
214
subsequently used (and cleared) in get_authenticated_user for
217
if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
218
raise Exception("This service does not support oauth_callback")
219
http = httpclient.AsyncHTTPClient()
220
http.fetch(self._oauth_request_token_url(), self.async_callback(
221
self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri))
223
def get_authenticated_user(self, callback):
224
"""Gets the OAuth authorized user and access token on callback.
226
This method should be called from the handler for your registered
227
OAuth Callback URL to complete the registration process. We call
228
callback with the authenticated user, which in addition to standard
229
attributes like 'name' includes the 'access_key' attribute, which
230
contains the OAuth access you can use to make authorized requests
231
to this service on behalf of the user.
233
request_key = self.get_argument("oauth_token")
234
request_cookie = self.get_cookie("_oauth_request_token")
235
if not request_cookie:
236
_log.warning("Missing OAuth request token cookie")
239
cookie_key, cookie_secret = request_cookie.split("|")
240
if cookie_key != request_key:
241
_log.warning("Request token does not match cookie")
244
token = dict(key=cookie_key, secret=cookie_secret)
245
http = httpclient.AsyncHTTPClient()
246
http.fetch(self._oauth_access_token_url(token), self.async_callback(
247
self._on_access_token, callback))
249
def _oauth_request_token_url(self):
250
consumer_token = self._oauth_consumer_token()
251
url = self._OAUTH_REQUEST_TOKEN_URL
253
oauth_consumer_key=consumer_token["key"],
254
oauth_signature_method="HMAC-SHA1",
255
oauth_timestamp=str(int(time.time())),
256
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
259
signature = _oauth_signature(consumer_token, "GET", url, args)
260
args["oauth_signature"] = signature
261
return url + "?" + urllib.urlencode(args)
263
def _on_request_token(self, authorize_url, callback_uri, response):
265
raise Exception("Could not get request token")
266
request_token = _oauth_parse_response(response.body)
267
data = "|".join([request_token["key"], request_token["secret"]])
268
self.set_cookie("_oauth_request_token", data)
269
args = dict(oauth_token=request_token["key"])
271
args["oauth_callback"] = urlparse.urljoin(
272
self.request.full_url(), callback_uri)
273
self.redirect(authorize_url + "?" + urllib.urlencode(args))
275
def _oauth_access_token_url(self, request_token):
276
consumer_token = self._oauth_consumer_token()
277
url = self._OAUTH_ACCESS_TOKEN_URL
279
oauth_consumer_key=consumer_token["key"],
280
oauth_token=request_token["key"],
281
oauth_signature_method="HMAC-SHA1",
282
oauth_timestamp=str(int(time.time())),
283
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
286
signature = _oauth_signature(consumer_token, "GET", url, args,
288
args["oauth_signature"] = signature
289
return url + "?" + urllib.urlencode(args)
291
def _on_access_token(self, callback, response):
293
_log.warning("Could not fetch access token")
296
access_token = _oauth_parse_response(response.body)
297
user = self._oauth_get_user(access_token, self.async_callback(
298
self._on_oauth_get_user, access_token, callback))
300
def _oauth_get_user(self, access_token, callback):
301
raise NotImplementedError()
303
def _on_oauth_get_user(self, access_token, callback, user):
307
user["access_token"] = access_token
310
def _oauth_request_parameters(self, url, access_token, parameters={},
312
"""Returns the OAuth parameters as a dict for the given request.
314
parameters should include all POST arguments and query string arguments
315
that will be sent with the request.
317
consumer_token = self._oauth_consumer_token()
319
oauth_consumer_key=consumer_token["key"],
320
oauth_token=access_token["key"],
321
oauth_signature_method="HMAC-SHA1",
322
oauth_timestamp=str(int(time.time())),
323
oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
327
args.update(base_args)
328
args.update(parameters)
329
signature = _oauth_signature(consumer_token, method, url, args,
331
base_args["oauth_signature"] = signature
335
class TwitterMixin(OAuthMixin):
336
"""Twitter OAuth authentication.
338
To authenticate with Twitter, register your application with
339
Twitter at http://twitter.com/apps. Then copy your Consumer Key and
340
Consumer Secret to the application settings 'twitter_consumer_key' and
341
'twitter_consumer_secret'. Use this Mixin on the handler for the URL
342
you registered as your application's Callback URL.
344
When your application is set up, you can use this Mixin like this
345
to authenticate the user with Twitter and get access to their stream:
347
class TwitterHandler(tornado.web.RequestHandler,
348
tornado.auth.TwitterMixin):
349
@tornado.web.asynchronous
351
if self.get_argument("oauth_token", None):
352
self.get_authenticated_user(self.async_callback(self._on_auth))
354
self.authorize_redirect()
356
def _on_auth(self, user):
358
raise tornado.web.HTTPError(500, "Twitter auth failed")
359
# Save the user using, e.g., set_secure_cookie()
361
The user object returned by get_authenticated_user() includes the
362
attributes 'username', 'name', and all of the custom Twitter user
363
attributes describe at
364
http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
365
in addition to 'access_token'. You should save the access token with
366
the user; it is required to make requests on behalf of the user later
367
with twitter_request().
369
_OAUTH_REQUEST_TOKEN_URL = "http://twitter.com/oauth/request_token"
370
_OAUTH_ACCESS_TOKEN_URL = "http://twitter.com/oauth/access_token"
371
_OAUTH_AUTHORIZE_URL = "http://twitter.com/oauth/authorize"
372
_OAUTH_AUTHENTICATE_URL = "http://twitter.com/oauth/authenticate"
373
_OAUTH_NO_CALLBACKS = True
375
def authenticate_redirect(self):
376
"""Just like authorize_redirect(), but auto-redirects if authorized.
378
This is generally the right interface to use if you are using
379
Twitter for single-sign on.
381
http = httpclient.AsyncHTTPClient()
382
http.fetch(self._oauth_request_token_url(), self.async_callback(
383
self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
385
def twitter_request(self, path, callback, access_token=None,
386
post_args=None, **args):
387
"""Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
389
The path should not include the format (we automatically append
390
".json" and parse the JSON output).
392
If the request is a POST, post_args should be provided. Query
393
string arguments should be given as keyword arguments.
395
All the Twitter methods are documented at
396
http://apiwiki.twitter.com/Twitter-API-Documentation.
398
Many methods require an OAuth access token which you can obtain
399
through authorize_redirect() and get_authenticated_user(). The
400
user returned through that process includes an 'access_token'
401
attribute that can be used to make authenticated requests via
402
this method. Example usage:
404
class MainHandler(tornado.web.RequestHandler,
405
tornado.auth.TwitterMixin):
406
@tornado.web.authenticated
407
@tornado.web.asynchronous
409
self.twitter_request(
411
post_args={"status": "Testing Tornado Web Server"},
412
access_token=user["access_token"],
413
callback=self.async_callback(self._on_post))
415
def _on_post(self, new_entry):
417
# Call failed; perhaps missing permission?
418
self.authorize_redirect()
420
self.finish("Posted a message!")
423
# Add the OAuth resource request signature if we have credentials
424
url = "http://twitter.com" + path + ".json"
427
all_args.update(args)
428
all_args.update(post_args or {})
429
consumer_token = self._oauth_consumer_token()
430
method = "POST" if post_args is not None else "GET"
431
oauth = self._oauth_request_parameters(
432
url, access_token, all_args, method=method)
434
if args: url += "?" + urllib.urlencode(args)
435
callback = self.async_callback(self._on_twitter_request, callback)
436
http = httpclient.AsyncHTTPClient()
437
if post_args is not None:
438
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
441
http.fetch(url, callback=callback)
443
def _on_twitter_request(self, callback, response):
445
_log.warning("Error response %s fetching %s", response.error,
446
response.request.url)
449
callback(escape.json_decode(response.body))
451
def _oauth_consumer_token(self):
452
self.require_setting("twitter_consumer_key", "Twitter OAuth")
453
self.require_setting("twitter_consumer_secret", "Twitter OAuth")
455
key=self.settings["twitter_consumer_key"],
456
secret=self.settings["twitter_consumer_secret"])
458
def _oauth_get_user(self, access_token, callback):
459
callback = self.async_callback(self._parse_user_response, callback)
460
self.twitter_request(
461
"/users/show/" + access_token["screen_name"],
462
access_token=access_token, callback=callback)
464
def _parse_user_response(self, callback, user):
466
user["username"] = user["screen_name"]
470
class FriendFeedMixin(OAuthMixin):
471
"""FriendFeed OAuth authentication.
473
To authenticate with FriendFeed, register your application with
474
FriendFeed at http://friendfeed.com/api/applications. Then
475
copy your Consumer Key and Consumer Secret to the application settings
476
'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
477
this Mixin on the handler for the URL you registered as your
478
application's Callback URL.
480
When your application is set up, you can use this Mixin like this
481
to authenticate the user with FriendFeed and get access to their feed:
483
class FriendFeedHandler(tornado.web.RequestHandler,
484
tornado.auth.FriendFeedMixin):
485
@tornado.web.asynchronous
487
if self.get_argument("oauth_token", None):
488
self.get_authenticated_user(self.async_callback(self._on_auth))
490
self.authorize_redirect()
492
def _on_auth(self, user):
494
raise tornado.web.HTTPError(500, "FriendFeed auth failed")
495
# Save the user using, e.g., set_secure_cookie()
497
The user object returned by get_authenticated_user() includes the
498
attributes 'username', 'name', and 'description' in addition to
499
'access_token'. You should save the access token with the user;
500
it is required to make requests on behalf of the user later with
501
friendfeed_request().
503
_OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
504
_OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
505
_OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
506
_OAUTH_NO_CALLBACKS = True
508
def friendfeed_request(self, path, callback, access_token=None,
509
post_args=None, **args):
510
"""Fetches the given relative API path, e.g., "/bret/friends"
512
If the request is a POST, post_args should be provided. Query
513
string arguments should be given as keyword arguments.
515
All the FriendFeed methods are documented at
516
http://friendfeed.com/api/documentation.
518
Many methods require an OAuth access token which you can obtain
519
through authorize_redirect() and get_authenticated_user(). The
520
user returned through that process includes an 'access_token'
521
attribute that can be used to make authenticated requests via
522
this method. Example usage:
524
class MainHandler(tornado.web.RequestHandler,
525
tornado.auth.FriendFeedMixin):
526
@tornado.web.authenticated
527
@tornado.web.asynchronous
529
self.friendfeed_request(
531
post_args={"body": "Testing Tornado Web Server"},
532
access_token=self.current_user["access_token"],
533
callback=self.async_callback(self._on_post))
535
def _on_post(self, new_entry):
537
# Call failed; perhaps missing permission?
538
self.authorize_redirect()
540
self.finish("Posted a message!")
543
# Add the OAuth resource request signature if we have credentials
544
url = "http://friendfeed-api.com/v2" + path
547
all_args.update(args)
548
all_args.update(post_args or {})
549
consumer_token = self._oauth_consumer_token()
550
method = "POST" if post_args is not None else "GET"
551
oauth = self._oauth_request_parameters(
552
url, access_token, all_args, method=method)
554
if args: url += "?" + urllib.urlencode(args)
555
callback = self.async_callback(self._on_friendfeed_request, callback)
556
http = httpclient.AsyncHTTPClient()
557
if post_args is not None:
558
http.fetch(url, method="POST", body=urllib.urlencode(post_args),
561
http.fetch(url, callback=callback)
563
def _on_friendfeed_request(self, callback, response):
565
_log.warning("Error response %s fetching %s", response.error,
566
response.request.url)
569
callback(escape.json_decode(response.body))
571
def _oauth_consumer_token(self):
572
self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
573
self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
575
key=self.settings["friendfeed_consumer_key"],
576
secret=self.settings["friendfeed_consumer_secret"])
578
def _oauth_get_user(self, access_token, callback):
579
callback = self.async_callback(self._parse_user_response, callback)
580
self.friendfeed_request(
581
"/feedinfo/" + access_token["username"],
582
include="id,name,description", access_token=access_token,
585
def _parse_user_response(self, callback, user):
587
user["username"] = user["id"]
591
class GoogleMixin(OpenIdMixin, OAuthMixin):
592
"""Google Open ID / OAuth authentication.
594
No application registration is necessary to use Google for authentication
595
or to access Google resources on behalf of a user. To authenticate with
596
Google, redirect with authenticate_redirect(). On return, parse the
597
response with get_authenticated_user(). We send a dict containing the
598
values for the user, including 'email', 'name', and 'locale'.
601
class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
602
@tornado.web.asynchronous
604
if self.get_argument("openid.mode", None):
605
self.get_authenticated_user(self.async_callback(self._on_auth))
607
self.authenticate_redirect()
609
def _on_auth(self, user):
611
raise tornado.web.HTTPError(500, "Google auth failed")
612
# Save the user with, e.g., set_secure_cookie()
615
_OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
616
_OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
618
def authorize_redirect(self, oauth_scope, callback_uri=None,
619
ax_attrs=["name","email","language","username"]):
620
"""Authenticates and authorizes for the given Google resource.
622
Some of the available resources are:
624
Gmail Contacts - http://www.google.com/m8/feeds/
625
Calendar - http://www.google.com/calendar/feeds/
626
Finance - http://finance.google.com/finance/feeds/
628
You can authorize multiple resources by separating the resource
631
callback_uri = callback_uri or self.request.path
632
args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
633
oauth_scope=oauth_scope)
634
self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
636
def get_authenticated_user(self, callback):
637
"""Fetches the authenticated user data upon redirect."""
638
# Look to see if we are doing combined OpenID/OAuth
640
for name, values in self.request.arguments.iteritems():
641
if name.startswith("openid.ns.") and \
642
values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
645
token = self.get_argument("openid." + oauth_ns + ".request_token", "")
647
http = httpclient.AsyncHTTPClient()
648
token = dict(key=token, secret="")
649
http.fetch(self._oauth_access_token_url(token),
650
self.async_callback(self._on_access_token, callback))
652
OpenIdMixin.get_authenticated_user(self, callback)
654
def _oauth_consumer_token(self):
655
self.require_setting("google_consumer_key", "Google OAuth")
656
self.require_setting("google_consumer_secret", "Google OAuth")
658
key=self.settings["google_consumer_key"],
659
secret=self.settings["google_consumer_secret"])
661
def _oauth_get_user(self, access_token, callback):
662
OpenIdMixin.get_authenticated_user(self, callback)
665
class FacebookMixin(object):
666
"""Facebook Connect authentication.
668
To authenticate with Facebook, register your application with
669
Facebook at http://www.facebook.com/developers/apps.php. Then
670
copy your API Key and Application Secret to the application settings
671
'facebook_api_key' and 'facebook_secret'.
673
When your application is set up, you can use this Mixin like this
674
to authenticate the user with Facebook:
676
class FacebookHandler(tornado.web.RequestHandler,
677
tornado.auth.FacebookMixin):
678
@tornado.web.asynchronous
680
if self.get_argument("session", None):
681
self.get_authenticated_user(self.async_callback(self._on_auth))
683
self.authenticate_redirect()
685
def _on_auth(self, user):
687
raise tornado.web.HTTPError(500, "Facebook auth failed")
688
# Save the user using, e.g., set_secure_cookie()
690
The user object returned by get_authenticated_user() includes the
691
attributes 'facebook_uid' and 'name' in addition to session attributes
692
like 'session_key'. You should save the session key with the user; it is
693
required to make requests on behalf of the user later with
696
def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
697
extended_permissions=None):
698
"""Authenticates/installs this app for the current user."""
699
self.require_setting("facebook_api_key", "Facebook Connect")
700
callback_uri = callback_uri or self.request.path
702
"api_key": self.settings["facebook_api_key"],
706
"next": urlparse.urljoin(self.request.full_url(), callback_uri),
707
"return_session": "true",
710
args["cancel_url"] = urlparse.urljoin(
711
self.request.full_url(), cancel_uri)
712
if extended_permissions:
713
if isinstance(extended_permissions, basestring):
714
extended_permissions = [extended_permissions]
715
args["req_perms"] = ",".join(extended_permissions)
716
self.redirect("http://www.facebook.com/login.php?" +
717
urllib.urlencode(args))
719
def authorize_redirect(self, extended_permissions, callback_uri=None,
721
"""Redirects to an authorization request for the given FB resource.
723
The available resource names are listed at
724
http://wiki.developers.facebook.com/index.php/Extended_permission.
725
The most common resource types include:
732
extended_permissions can be a single permission name or a list of
733
names. To get the session secret and session key, call
734
get_authenticated_user() just as you would with
735
authenticate_redirect().
737
self.authenticate_redirect(callback_uri, cancel_uri,
738
extended_permissions)
740
def get_authenticated_user(self, callback):
741
"""Fetches the authenticated Facebook user.
743
The authenticated user includes the special Facebook attributes
744
'session_key' and 'facebook_uid' in addition to the standard
745
user attributes like 'name'.
747
self.require_setting("facebook_api_key", "Facebook Connect")
748
session = escape.json_decode(self.get_argument("session"))
749
self.facebook_request(
750
method="facebook.users.getInfo",
751
callback=self.async_callback(
752
self._on_get_user_info, callback, session),
753
session_key=session["session_key"],
755
fields="uid,first_name,last_name,name,locale,pic_square," \
756
"profile_url,username")
758
def facebook_request(self, method, callback, **args):
759
"""Makes a Facebook API REST request.
761
We automatically include the Facebook API key and signature, but
762
it is the callers responsibility to include 'session_key' and any
763
other required arguments to the method.
765
The available Facebook methods are documented here:
766
http://wiki.developers.facebook.com/index.php/API
768
Here is an example for the stream.get() method:
770
class MainHandler(tornado.web.RequestHandler,
771
tornado.auth.FacebookMixin):
772
@tornado.web.authenticated
773
@tornado.web.asynchronous
775
self.facebook_request(
777
callback=self.async_callback(self._on_stream),
778
session_key=self.current_user["session_key"])
780
def _on_stream(self, stream):
782
# Not authorized to read the stream yet?
783
self.redirect(self.authorize_redirect("read_stream"))
785
self.render("stream.html", stream=stream)
788
self.require_setting("facebook_api_key", "Facebook Connect")
789
self.require_setting("facebook_secret", "Facebook Connect")
790
if not method.startswith("facebook."):
791
method = "facebook." + method
792
args["api_key"] = self.settings["facebook_api_key"]
794
args["method"] = method
795
args["call_id"] = str(long(time.time() * 1e6))
796
args["format"] = "json"
797
args["sig"] = self._signature(args)
798
url = "http://api.facebook.com/restserver.php?" + \
799
urllib.urlencode(args)
800
http = httpclient.AsyncHTTPClient()
801
http.fetch(url, callback=self.async_callback(
802
self._parse_response, callback))
804
def _on_get_user_info(self, callback, session, users):
809
"name": users[0]["name"],
810
"first_name": users[0]["first_name"],
811
"last_name": users[0]["last_name"],
812
"uid": users[0]["uid"],
813
"locale": users[0]["locale"],
814
"pic_square": users[0]["pic_square"],
815
"profile_url": users[0]["profile_url"],
816
"username": users[0].get("username"),
817
"session_key": session["session_key"],
818
"session_expires": session["expires"],
821
def _parse_response(self, callback, response):
823
_log.warning("HTTP error from Facebook: %s", response.error)
827
json = escape.json_decode(response.body)
829
_log.warning("Invalid JSON from Facebook: %r", response.body)
832
if isinstance(json, dict) and json.get("error_code"):
833
_log.warning("Facebook error: %d: %r", json["error_code"],
834
json.get("error_msg"))
839
def _signature(self, args):
840
parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
841
body = "".join(parts) + self.settings["facebook_secret"]
842
if isinstance(body, unicode): body = body.encode("utf-8")
843
return hashlib.md5(body).hexdigest()
846
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
847
"""Calculates the HMAC-SHA1 OAuth signature for the given request.
849
See http://oauth.net/core/1.0/#signing_process
851
parts = urlparse.urlparse(url)
852
scheme, netloc, path = parts[:3]
853
normalized_url = scheme.lower() + "://" + netloc.lower() + path
856
base_elems.append(method.upper())
857
base_elems.append(normalized_url)
858
base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
859
for k, v in sorted(parameters.items())))
860
base_string = "&".join(_oauth_escape(e) for e in base_elems)
862
key_elems = [consumer_token["secret"]]
863
key_elems.append(token["secret"] if token else "")
864
key = "&".join(key_elems)
866
hash = hmac.new(key, base_string, hashlib.sha1)
867
return binascii.b2a_base64(hash.digest())[:-1]
870
def _oauth_escape(val):
871
if isinstance(val, unicode):
872
val = val.encode("utf-8")
873
return urllib.quote(val, safe="~")
876
def _oauth_parse_response(body):
877
p = cgi.parse_qs(body, keep_blank_values=False)
878
token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0])
880
# Add the extra parameters the Provider included to the token
881
special = ("oauth_token", "oauth_token_secret")
882
token.update((k, p[k][0]) for k in p if k not in special)