00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017 """Implementations of various third-party authentication schemes.
00018
00019 All the classes in this file are class Mixins designed to be used with
00020 web.py RequestHandler classes. The primary methods for each service are
00021 authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
00022 The former should be called to redirect the user to, e.g., the OpenID
00023 authentication page on the third party service, and the latter should
00024 be called upon return to get the user data from the data returned by
00025 the third party service.
00026
00027 They all take slightly different arguments due to the fact all these
00028 services implement authentication and authorization slightly differently.
00029 See the individual service classes below for complete documentation.
00030
00031 Example usage for Google OpenID::
00032
00033 class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
00034 @tornado.web.asynchronous
00035 def get(self):
00036 if self.get_argument("openid.mode", None):
00037 self.get_authenticated_user(self.async_callback(self._on_auth))
00038 return
00039 self.authenticate_redirect()
00040
00041 def _on_auth(self, user):
00042 if not user:
00043 raise tornado.web.HTTPError(500, "Google auth failed")
00044 # Save the user with, e.g., set_secure_cookie()
00045 """
00046
00047 from __future__ import absolute_import, division, with_statement
00048
00049 import base64
00050 import binascii
00051 import hashlib
00052 import hmac
00053 import logging
00054 import time
00055 import urllib
00056 import urlparse
00057 import uuid
00058
00059 from tornado import httpclient
00060 from tornado import escape
00061 from tornado.httputil import url_concat
00062 from tornado.util import bytes_type, b
00063
00064
00065 class OpenIdMixin(object):
00066 """Abstract implementation of OpenID and Attribute Exchange.
00067
00068 See GoogleMixin below for example implementations.
00069 """
00070 def authenticate_redirect(self, callback_uri=None,
00071 ax_attrs=["name", "email", "language", "username"]):
00072 """Returns the authentication URL for this service.
00073
00074 After authentication, the service will redirect back to the given
00075 callback URI.
00076
00077 We request the given attributes for the authenticated user by
00078 default (name, email, language, and username). If you don't need
00079 all those attributes for your app, you can request fewer with
00080 the ax_attrs keyword argument.
00081 """
00082 callback_uri = callback_uri or self.request.uri
00083 args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
00084 self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
00085
00086 def get_authenticated_user(self, callback, http_client=None):
00087 """Fetches the authenticated user data upon redirect.
00088
00089 This method should be called by the handler that receives the
00090 redirect from the authenticate_redirect() or authorize_redirect()
00091 methods.
00092 """
00093
00094 args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
00095 args["openid.mode"] = u"check_authentication"
00096 url = self._OPENID_ENDPOINT
00097 if http_client is None:
00098 http_client = httpclient.AsyncHTTPClient()
00099 http_client.fetch(url, self.async_callback(
00100 self._on_authentication_verified, callback),
00101 method="POST", body=urllib.urlencode(args))
00102
00103 def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
00104 url = urlparse.urljoin(self.request.full_url(), callback_uri)
00105 args = {
00106 "openid.ns": "http://specs.openid.net/auth/2.0",
00107 "openid.claimed_id":
00108 "http://specs.openid.net/auth/2.0/identifier_select",
00109 "openid.identity":
00110 "http://specs.openid.net/auth/2.0/identifier_select",
00111 "openid.return_to": url,
00112 "openid.realm": urlparse.urljoin(url, '/'),
00113 "openid.mode": "checkid_setup",
00114 }
00115 if ax_attrs:
00116 args.update({
00117 "openid.ns.ax": "http://openid.net/srv/ax/1.0",
00118 "openid.ax.mode": "fetch_request",
00119 })
00120 ax_attrs = set(ax_attrs)
00121 required = []
00122 if "name" in ax_attrs:
00123 ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
00124 required += ["firstname", "fullname", "lastname"]
00125 args.update({
00126 "openid.ax.type.firstname":
00127 "http://axschema.org/namePerson/first",
00128 "openid.ax.type.fullname":
00129 "http://axschema.org/namePerson",
00130 "openid.ax.type.lastname":
00131 "http://axschema.org/namePerson/last",
00132 })
00133 known_attrs = {
00134 "email": "http://axschema.org/contact/email",
00135 "language": "http://axschema.org/pref/language",
00136 "username": "http://axschema.org/namePerson/friendly",
00137 }
00138 for name in ax_attrs:
00139 args["openid.ax.type." + name] = known_attrs[name]
00140 required.append(name)
00141 args["openid.ax.required"] = ",".join(required)
00142 if oauth_scope:
00143 args.update({
00144 "openid.ns.oauth":
00145 "http://specs.openid.net/extensions/oauth/1.0",
00146 "openid.oauth.consumer": self.request.host.split(":")[0],
00147 "openid.oauth.scope": oauth_scope,
00148 })
00149 return args
00150
00151 def _on_authentication_verified(self, callback, response):
00152 if response.error or b("is_valid:true") not in response.body:
00153 logging.warning("Invalid OpenID response: %s", response.error or
00154 response.body)
00155 callback(None)
00156 return
00157
00158
00159 ax_ns = None
00160 for name in self.request.arguments.iterkeys():
00161 if name.startswith("openid.ns.") and \
00162 self.get_argument(name) == u"http://openid.net/srv/ax/1.0":
00163 ax_ns = name[10:]
00164 break
00165
00166 def get_ax_arg(uri):
00167 if not ax_ns:
00168 return u""
00169 prefix = "openid." + ax_ns + ".type."
00170 ax_name = None
00171 for name in self.request.arguments.iterkeys():
00172 if self.get_argument(name) == uri and name.startswith(prefix):
00173 part = name[len(prefix):]
00174 ax_name = "openid." + ax_ns + ".value." + part
00175 break
00176 if not ax_name:
00177 return u""
00178 return self.get_argument(ax_name, u"")
00179
00180 email = get_ax_arg("http://axschema.org/contact/email")
00181 name = get_ax_arg("http://axschema.org/namePerson")
00182 first_name = get_ax_arg("http://axschema.org/namePerson/first")
00183 last_name = get_ax_arg("http://axschema.org/namePerson/last")
00184 username = get_ax_arg("http://axschema.org/namePerson/friendly")
00185 locale = get_ax_arg("http://axschema.org/pref/language").lower()
00186 user = dict()
00187 name_parts = []
00188 if first_name:
00189 user["first_name"] = first_name
00190 name_parts.append(first_name)
00191 if last_name:
00192 user["last_name"] = last_name
00193 name_parts.append(last_name)
00194 if name:
00195 user["name"] = name
00196 elif name_parts:
00197 user["name"] = u" ".join(name_parts)
00198 elif email:
00199 user["name"] = email.split("@")[0]
00200 if email:
00201 user["email"] = email
00202 if locale:
00203 user["locale"] = locale
00204 if username:
00205 user["username"] = username
00206 callback(user)
00207
00208
00209 class OAuthMixin(object):
00210 """Abstract implementation of OAuth.
00211
00212 See TwitterMixin and FriendFeedMixin below for example implementations.
00213 """
00214
00215 def authorize_redirect(self, callback_uri=None, extra_params=None,
00216 http_client=None):
00217 """Redirects the user to obtain OAuth authorization for this service.
00218
00219 Twitter and FriendFeed both require that you register a Callback
00220 URL with your application. You should call this method to log the
00221 user in, and then call get_authenticated_user() in the handler
00222 you registered as your Callback URL to complete the authorization
00223 process.
00224
00225 This method sets a cookie called _oauth_request_token which is
00226 subsequently used (and cleared) in get_authenticated_user for
00227 security purposes.
00228 """
00229 if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
00230 raise Exception("This service does not support oauth_callback")
00231 if http_client is None:
00232 http_client = httpclient.AsyncHTTPClient()
00233 if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00234 http_client.fetch(
00235 self._oauth_request_token_url(callback_uri=callback_uri,
00236 extra_params=extra_params),
00237 self.async_callback(
00238 self._on_request_token,
00239 self._OAUTH_AUTHORIZE_URL,
00240 callback_uri))
00241 else:
00242 http_client.fetch(
00243 self._oauth_request_token_url(),
00244 self.async_callback(
00245 self._on_request_token, self._OAUTH_AUTHORIZE_URL,
00246 callback_uri))
00247
00248 def get_authenticated_user(self, callback, http_client=None):
00249 """Gets the OAuth authorized user and access token on callback.
00250
00251 This method should be called from the handler for your registered
00252 OAuth Callback URL to complete the registration process. We call
00253 callback with the authenticated user, which in addition to standard
00254 attributes like 'name' includes the 'access_key' attribute, which
00255 contains the OAuth access you can use to make authorized requests
00256 to this service on behalf of the user.
00257
00258 """
00259 request_key = escape.utf8(self.get_argument("oauth_token"))
00260 oauth_verifier = self.get_argument("oauth_verifier", None)
00261 request_cookie = self.get_cookie("_oauth_request_token")
00262 if not request_cookie:
00263 logging.warning("Missing OAuth request token cookie")
00264 callback(None)
00265 return
00266 self.clear_cookie("_oauth_request_token")
00267 cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")]
00268 if cookie_key != request_key:
00269 logging.info((cookie_key, request_key, request_cookie))
00270 logging.warning("Request token does not match cookie")
00271 callback(None)
00272 return
00273 token = dict(key=cookie_key, secret=cookie_secret)
00274 if oauth_verifier:
00275 token["verifier"] = oauth_verifier
00276 if http_client is None:
00277 http_client = httpclient.AsyncHTTPClient()
00278 http_client.fetch(self._oauth_access_token_url(token),
00279 self.async_callback(self._on_access_token, callback))
00280
00281 def _oauth_request_token_url(self, callback_uri=None, extra_params=None):
00282 consumer_token = self._oauth_consumer_token()
00283 url = self._OAUTH_REQUEST_TOKEN_URL
00284 args = dict(
00285 oauth_consumer_key=consumer_token["key"],
00286 oauth_signature_method="HMAC-SHA1",
00287 oauth_timestamp=str(int(time.time())),
00288 oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
00289 oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
00290 )
00291 if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00292 if callback_uri:
00293 args["oauth_callback"] = urlparse.urljoin(
00294 self.request.full_url(), callback_uri)
00295 if extra_params:
00296 args.update(extra_params)
00297 signature = _oauth10a_signature(consumer_token, "GET", url, args)
00298 else:
00299 signature = _oauth_signature(consumer_token, "GET", url, args)
00300
00301 args["oauth_signature"] = signature
00302 return url + "?" + urllib.urlencode(args)
00303
00304 def _on_request_token(self, authorize_url, callback_uri, response):
00305 if response.error:
00306 raise Exception("Could not get request token")
00307 request_token = _oauth_parse_response(response.body)
00308 data = (base64.b64encode(request_token["key"]) + b("|") +
00309 base64.b64encode(request_token["secret"]))
00310 self.set_cookie("_oauth_request_token", data)
00311 args = dict(oauth_token=request_token["key"])
00312 if callback_uri:
00313 args["oauth_callback"] = urlparse.urljoin(
00314 self.request.full_url(), callback_uri)
00315 self.redirect(authorize_url + "?" + urllib.urlencode(args))
00316
00317 def _oauth_access_token_url(self, request_token):
00318 consumer_token = self._oauth_consumer_token()
00319 url = self._OAUTH_ACCESS_TOKEN_URL
00320 args = dict(
00321 oauth_consumer_key=consumer_token["key"],
00322 oauth_token=request_token["key"],
00323 oauth_signature_method="HMAC-SHA1",
00324 oauth_timestamp=str(int(time.time())),
00325 oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
00326 oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
00327 )
00328 if "verifier" in request_token:
00329 args["oauth_verifier"] = request_token["verifier"]
00330
00331 if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00332 signature = _oauth10a_signature(consumer_token, "GET", url, args,
00333 request_token)
00334 else:
00335 signature = _oauth_signature(consumer_token, "GET", url, args,
00336 request_token)
00337
00338 args["oauth_signature"] = signature
00339 return url + "?" + urllib.urlencode(args)
00340
00341 def _on_access_token(self, callback, response):
00342 if response.error:
00343 logging.warning("Could not fetch access token")
00344 callback(None)
00345 return
00346
00347 access_token = _oauth_parse_response(response.body)
00348 self._oauth_get_user(access_token, self.async_callback(
00349 self._on_oauth_get_user, access_token, callback))
00350
00351 def _oauth_get_user(self, access_token, callback):
00352 raise NotImplementedError()
00353
00354 def _on_oauth_get_user(self, access_token, callback, user):
00355 if not user:
00356 callback(None)
00357 return
00358 user["access_token"] = access_token
00359 callback(user)
00360
00361 def _oauth_request_parameters(self, url, access_token, parameters={},
00362 method="GET"):
00363 """Returns the OAuth parameters as a dict for the given request.
00364
00365 parameters should include all POST arguments and query string arguments
00366 that will be sent with the request.
00367 """
00368 consumer_token = self._oauth_consumer_token()
00369 base_args = dict(
00370 oauth_consumer_key=consumer_token["key"],
00371 oauth_token=access_token["key"],
00372 oauth_signature_method="HMAC-SHA1",
00373 oauth_timestamp=str(int(time.time())),
00374 oauth_nonce=binascii.b2a_hex(uuid.uuid4().bytes),
00375 oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
00376 )
00377 args = {}
00378 args.update(base_args)
00379 args.update(parameters)
00380 if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00381 signature = _oauth10a_signature(consumer_token, method, url, args,
00382 access_token)
00383 else:
00384 signature = _oauth_signature(consumer_token, method, url, args,
00385 access_token)
00386 base_args["oauth_signature"] = signature
00387 return base_args
00388
00389
00390 class OAuth2Mixin(object):
00391 """Abstract implementation of OAuth v 2."""
00392
00393 def authorize_redirect(self, redirect_uri=None, client_id=None,
00394 client_secret=None, extra_params=None):
00395 """Redirects the user to obtain OAuth authorization for this service.
00396
00397 Some providers require that you register a Callback
00398 URL with your application. You should call this method to log the
00399 user in, and then call get_authenticated_user() in the handler
00400 you registered as your Callback URL to complete the authorization
00401 process.
00402 """
00403 args = {
00404 "redirect_uri": redirect_uri,
00405 "client_id": client_id
00406 }
00407 if extra_params:
00408 args.update(extra_params)
00409 self.redirect(
00410 url_concat(self._OAUTH_AUTHORIZE_URL, args))
00411
00412 def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
00413 client_secret=None, code=None,
00414 extra_params=None):
00415 url = self._OAUTH_ACCESS_TOKEN_URL
00416 args = dict(
00417 redirect_uri=redirect_uri,
00418 code=code,
00419 client_id=client_id,
00420 client_secret=client_secret,
00421 )
00422 if extra_params:
00423 args.update(extra_params)
00424 return url_concat(url, args)
00425
00426
00427 class TwitterMixin(OAuthMixin):
00428 """Twitter OAuth authentication.
00429
00430 To authenticate with Twitter, register your application with
00431 Twitter at http://twitter.com/apps. Then copy your Consumer Key and
00432 Consumer Secret to the application settings 'twitter_consumer_key' and
00433 'twitter_consumer_secret'. Use this Mixin on the handler for the URL
00434 you registered as your application's Callback URL.
00435
00436 When your application is set up, you can use this Mixin like this
00437 to authenticate the user with Twitter and get access to their stream::
00438
00439 class TwitterHandler(tornado.web.RequestHandler,
00440 tornado.auth.TwitterMixin):
00441 @tornado.web.asynchronous
00442 def get(self):
00443 if self.get_argument("oauth_token", None):
00444 self.get_authenticated_user(self.async_callback(self._on_auth))
00445 return
00446 self.authorize_redirect()
00447
00448 def _on_auth(self, user):
00449 if not user:
00450 raise tornado.web.HTTPError(500, "Twitter auth failed")
00451 # Save the user using, e.g., set_secure_cookie()
00452
00453 The user object returned by get_authenticated_user() includes the
00454 attributes 'username', 'name', and all of the custom Twitter user
00455 attributes describe at
00456 http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
00457 in addition to 'access_token'. You should save the access token with
00458 the user; it is required to make requests on behalf of the user later
00459 with twitter_request().
00460 """
00461 _OAUTH_REQUEST_TOKEN_URL = "http://api.twitter.com/oauth/request_token"
00462 _OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token"
00463 _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
00464 _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
00465 _OAUTH_NO_CALLBACKS = False
00466
00467 def authenticate_redirect(self, callback_uri=None):
00468 """Just like authorize_redirect(), but auto-redirects if authorized.
00469
00470 This is generally the right interface to use if you are using
00471 Twitter for single-sign on.
00472 """
00473 http = httpclient.AsyncHTTPClient()
00474 http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback(
00475 self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
00476
00477 def twitter_request(self, path, callback, access_token=None,
00478 post_args=None, **args):
00479 """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
00480
00481 The path should not include the format (we automatically append
00482 ".json" and parse the JSON output).
00483
00484 If the request is a POST, post_args should be provided. Query
00485 string arguments should be given as keyword arguments.
00486
00487 All the Twitter methods are documented at
00488 http://apiwiki.twitter.com/Twitter-API-Documentation.
00489
00490 Many methods require an OAuth access token which you can obtain
00491 through authorize_redirect() and get_authenticated_user(). The
00492 user returned through that process includes an 'access_token'
00493 attribute that can be used to make authenticated requests via
00494 this method. Example usage::
00495
00496 class MainHandler(tornado.web.RequestHandler,
00497 tornado.auth.TwitterMixin):
00498 @tornado.web.authenticated
00499 @tornado.web.asynchronous
00500 def get(self):
00501 self.twitter_request(
00502 "/statuses/update",
00503 post_args={"status": "Testing Tornado Web Server"},
00504 access_token=user["access_token"],
00505 callback=self.async_callback(self._on_post))
00506
00507 def _on_post(self, new_entry):
00508 if not new_entry:
00509 # Call failed; perhaps missing permission?
00510 self.authorize_redirect()
00511 return
00512 self.finish("Posted a message!")
00513
00514 """
00515 if path.startswith('http:') or path.startswith('https:'):
00516
00517
00518 url = path
00519 else:
00520 url = "http://api.twitter.com/1" + path + ".json"
00521
00522 if access_token:
00523 all_args = {}
00524 all_args.update(args)
00525 all_args.update(post_args or {})
00526 method = "POST" if post_args is not None else "GET"
00527 oauth = self._oauth_request_parameters(
00528 url, access_token, all_args, method=method)
00529 args.update(oauth)
00530 if args:
00531 url += "?" + urllib.urlencode(args)
00532 callback = self.async_callback(self._on_twitter_request, callback)
00533 http = httpclient.AsyncHTTPClient()
00534 if post_args is not None:
00535 http.fetch(url, method="POST", body=urllib.urlencode(post_args),
00536 callback=callback)
00537 else:
00538 http.fetch(url, callback=callback)
00539
00540 def _on_twitter_request(self, callback, response):
00541 if response.error:
00542 logging.warning("Error response %s fetching %s", response.error,
00543 response.request.url)
00544 callback(None)
00545 return
00546 callback(escape.json_decode(response.body))
00547
00548 def _oauth_consumer_token(self):
00549 self.require_setting("twitter_consumer_key", "Twitter OAuth")
00550 self.require_setting("twitter_consumer_secret", "Twitter OAuth")
00551 return dict(
00552 key=self.settings["twitter_consumer_key"],
00553 secret=self.settings["twitter_consumer_secret"])
00554
00555 def _oauth_get_user(self, access_token, callback):
00556 callback = self.async_callback(self._parse_user_response, callback)
00557 self.twitter_request(
00558 "/users/show/" + access_token["screen_name"],
00559 access_token=access_token, callback=callback)
00560
00561 def _parse_user_response(self, callback, user):
00562 if user:
00563 user["username"] = user["screen_name"]
00564 callback(user)
00565
00566
00567 class FriendFeedMixin(OAuthMixin):
00568 """FriendFeed OAuth authentication.
00569
00570 To authenticate with FriendFeed, register your application with
00571 FriendFeed at http://friendfeed.com/api/applications. Then
00572 copy your Consumer Key and Consumer Secret to the application settings
00573 'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
00574 this Mixin on the handler for the URL you registered as your
00575 application's Callback URL.
00576
00577 When your application is set up, you can use this Mixin like this
00578 to authenticate the user with FriendFeed and get access to their feed::
00579
00580 class FriendFeedHandler(tornado.web.RequestHandler,
00581 tornado.auth.FriendFeedMixin):
00582 @tornado.web.asynchronous
00583 def get(self):
00584 if self.get_argument("oauth_token", None):
00585 self.get_authenticated_user(self.async_callback(self._on_auth))
00586 return
00587 self.authorize_redirect()
00588
00589 def _on_auth(self, user):
00590 if not user:
00591 raise tornado.web.HTTPError(500, "FriendFeed auth failed")
00592 # Save the user using, e.g., set_secure_cookie()
00593
00594 The user object returned by get_authenticated_user() includes the
00595 attributes 'username', 'name', and 'description' in addition to
00596 'access_token'. You should save the access token with the user;
00597 it is required to make requests on behalf of the user later with
00598 friendfeed_request().
00599 """
00600 _OAUTH_VERSION = "1.0"
00601 _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
00602 _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
00603 _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
00604 _OAUTH_NO_CALLBACKS = True
00605 _OAUTH_VERSION = "1.0"
00606
00607 def friendfeed_request(self, path, callback, access_token=None,
00608 post_args=None, **args):
00609 """Fetches the given relative API path, e.g., "/bret/friends"
00610
00611 If the request is a POST, post_args should be provided. Query
00612 string arguments should be given as keyword arguments.
00613
00614 All the FriendFeed methods are documented at
00615 http://friendfeed.com/api/documentation.
00616
00617 Many methods require an OAuth access token which you can obtain
00618 through authorize_redirect() and get_authenticated_user(). The
00619 user returned through that process includes an 'access_token'
00620 attribute that can be used to make authenticated requests via
00621 this method. Example usage::
00622
00623 class MainHandler(tornado.web.RequestHandler,
00624 tornado.auth.FriendFeedMixin):
00625 @tornado.web.authenticated
00626 @tornado.web.asynchronous
00627 def get(self):
00628 self.friendfeed_request(
00629 "/entry",
00630 post_args={"body": "Testing Tornado Web Server"},
00631 access_token=self.current_user["access_token"],
00632 callback=self.async_callback(self._on_post))
00633
00634 def _on_post(self, new_entry):
00635 if not new_entry:
00636 # Call failed; perhaps missing permission?
00637 self.authorize_redirect()
00638 return
00639 self.finish("Posted a message!")
00640
00641 """
00642
00643 url = "http://friendfeed-api.com/v2" + path
00644 if access_token:
00645 all_args = {}
00646 all_args.update(args)
00647 all_args.update(post_args or {})
00648 method = "POST" if post_args is not None else "GET"
00649 oauth = self._oauth_request_parameters(
00650 url, access_token, all_args, method=method)
00651 args.update(oauth)
00652 if args:
00653 url += "?" + urllib.urlencode(args)
00654 callback = self.async_callback(self._on_friendfeed_request, callback)
00655 http = httpclient.AsyncHTTPClient()
00656 if post_args is not None:
00657 http.fetch(url, method="POST", body=urllib.urlencode(post_args),
00658 callback=callback)
00659 else:
00660 http.fetch(url, callback=callback)
00661
00662 def _on_friendfeed_request(self, callback, response):
00663 if response.error:
00664 logging.warning("Error response %s fetching %s", response.error,
00665 response.request.url)
00666 callback(None)
00667 return
00668 callback(escape.json_decode(response.body))
00669
00670 def _oauth_consumer_token(self):
00671 self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
00672 self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
00673 return dict(
00674 key=self.settings["friendfeed_consumer_key"],
00675 secret=self.settings["friendfeed_consumer_secret"])
00676
00677 def _oauth_get_user(self, access_token, callback):
00678 callback = self.async_callback(self._parse_user_response, callback)
00679 self.friendfeed_request(
00680 "/feedinfo/" + access_token["username"],
00681 include="id,name,description", access_token=access_token,
00682 callback=callback)
00683
00684 def _parse_user_response(self, callback, user):
00685 if user:
00686 user["username"] = user["id"]
00687 callback(user)
00688
00689
00690 class GoogleMixin(OpenIdMixin, OAuthMixin):
00691 """Google Open ID / OAuth authentication.
00692
00693 No application registration is necessary to use Google for authentication
00694 or to access Google resources on behalf of a user. To authenticate with
00695 Google, redirect with authenticate_redirect(). On return, parse the
00696 response with get_authenticated_user(). We send a dict containing the
00697 values for the user, including 'email', 'name', and 'locale'.
00698 Example usage::
00699
00700 class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
00701 @tornado.web.asynchronous
00702 def get(self):
00703 if self.get_argument("openid.mode", None):
00704 self.get_authenticated_user(self.async_callback(self._on_auth))
00705 return
00706 self.authenticate_redirect()
00707
00708 def _on_auth(self, user):
00709 if not user:
00710 raise tornado.web.HTTPError(500, "Google auth failed")
00711 # Save the user with, e.g., set_secure_cookie()
00712
00713 """
00714 _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
00715 _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
00716
00717 def authorize_redirect(self, oauth_scope, callback_uri=None,
00718 ax_attrs=["name", "email", "language", "username"]):
00719 """Authenticates and authorizes for the given Google resource.
00720
00721 Some of the available resources are:
00722
00723 * Gmail Contacts - http://www.google.com/m8/feeds/
00724 * Calendar - http://www.google.com/calendar/feeds/
00725 * Finance - http://finance.google.com/finance/feeds/
00726
00727 You can authorize multiple resources by separating the resource
00728 URLs with a space.
00729 """
00730 callback_uri = callback_uri or self.request.uri
00731 args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
00732 oauth_scope=oauth_scope)
00733 self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
00734
00735 def get_authenticated_user(self, callback):
00736 """Fetches the authenticated user data upon redirect."""
00737
00738 oauth_ns = ""
00739 for name, values in self.request.arguments.iteritems():
00740 if name.startswith("openid.ns.") and \
00741 values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
00742 oauth_ns = name[10:]
00743 break
00744 token = self.get_argument("openid." + oauth_ns + ".request_token", "")
00745 if token:
00746 http = httpclient.AsyncHTTPClient()
00747 token = dict(key=token, secret="")
00748 http.fetch(self._oauth_access_token_url(token),
00749 self.async_callback(self._on_access_token, callback))
00750 else:
00751 OpenIdMixin.get_authenticated_user(self, callback)
00752
00753 def _oauth_consumer_token(self):
00754 self.require_setting("google_consumer_key", "Google OAuth")
00755 self.require_setting("google_consumer_secret", "Google OAuth")
00756 return dict(
00757 key=self.settings["google_consumer_key"],
00758 secret=self.settings["google_consumer_secret"])
00759
00760 def _oauth_get_user(self, access_token, callback):
00761 OpenIdMixin.get_authenticated_user(self, callback)
00762
00763
00764 class FacebookMixin(object):
00765 """Facebook Connect authentication.
00766
00767 New applications should consider using `FacebookGraphMixin` below instead
00768 of this class.
00769
00770 To authenticate with Facebook, register your application with
00771 Facebook at http://www.facebook.com/developers/apps.php. Then
00772 copy your API Key and Application Secret to the application settings
00773 'facebook_api_key' and 'facebook_secret'.
00774
00775 When your application is set up, you can use this Mixin like this
00776 to authenticate the user with Facebook::
00777
00778 class FacebookHandler(tornado.web.RequestHandler,
00779 tornado.auth.FacebookMixin):
00780 @tornado.web.asynchronous
00781 def get(self):
00782 if self.get_argument("session", None):
00783 self.get_authenticated_user(self.async_callback(self._on_auth))
00784 return
00785 self.authenticate_redirect()
00786
00787 def _on_auth(self, user):
00788 if not user:
00789 raise tornado.web.HTTPError(500, "Facebook auth failed")
00790 # Save the user using, e.g., set_secure_cookie()
00791
00792 The user object returned by get_authenticated_user() includes the
00793 attributes 'facebook_uid' and 'name' in addition to session attributes
00794 like 'session_key'. You should save the session key with the user; it is
00795 required to make requests on behalf of the user later with
00796 facebook_request().
00797 """
00798 def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
00799 extended_permissions=None):
00800 """Authenticates/installs this app for the current user."""
00801 self.require_setting("facebook_api_key", "Facebook Connect")
00802 callback_uri = callback_uri or self.request.uri
00803 args = {
00804 "api_key": self.settings["facebook_api_key"],
00805 "v": "1.0",
00806 "fbconnect": "true",
00807 "display": "page",
00808 "next": urlparse.urljoin(self.request.full_url(), callback_uri),
00809 "return_session": "true",
00810 }
00811 if cancel_uri:
00812 args["cancel_url"] = urlparse.urljoin(
00813 self.request.full_url(), cancel_uri)
00814 if extended_permissions:
00815 if isinstance(extended_permissions, (unicode, bytes_type)):
00816 extended_permissions = [extended_permissions]
00817 args["req_perms"] = ",".join(extended_permissions)
00818 self.redirect("http://www.facebook.com/login.php?" +
00819 urllib.urlencode(args))
00820
00821 def authorize_redirect(self, extended_permissions, callback_uri=None,
00822 cancel_uri=None):
00823 """Redirects to an authorization request for the given FB resource.
00824
00825 The available resource names are listed at
00826 http://wiki.developers.facebook.com/index.php/Extended_permission.
00827 The most common resource types include:
00828
00829 * publish_stream
00830 * read_stream
00831 * email
00832 * sms
00833
00834 extended_permissions can be a single permission name or a list of
00835 names. To get the session secret and session key, call
00836 get_authenticated_user() just as you would with
00837 authenticate_redirect().
00838 """
00839 self.authenticate_redirect(callback_uri, cancel_uri,
00840 extended_permissions)
00841
00842 def get_authenticated_user(self, callback):
00843 """Fetches the authenticated Facebook user.
00844
00845 The authenticated user includes the special Facebook attributes
00846 'session_key' and 'facebook_uid' in addition to the standard
00847 user attributes like 'name'.
00848 """
00849 self.require_setting("facebook_api_key", "Facebook Connect")
00850 session = escape.json_decode(self.get_argument("session"))
00851 self.facebook_request(
00852 method="facebook.users.getInfo",
00853 callback=self.async_callback(
00854 self._on_get_user_info, callback, session),
00855 session_key=session["session_key"],
00856 uids=session["uid"],
00857 fields="uid,first_name,last_name,name,locale,pic_square," \
00858 "profile_url,username")
00859
00860 def facebook_request(self, method, callback, **args):
00861 """Makes a Facebook API REST request.
00862
00863 We automatically include the Facebook API key and signature, but
00864 it is the callers responsibility to include 'session_key' and any
00865 other required arguments to the method.
00866
00867 The available Facebook methods are documented here:
00868 http://wiki.developers.facebook.com/index.php/API
00869
00870 Here is an example for the stream.get() method::
00871
00872 class MainHandler(tornado.web.RequestHandler,
00873 tornado.auth.FacebookMixin):
00874 @tornado.web.authenticated
00875 @tornado.web.asynchronous
00876 def get(self):
00877 self.facebook_request(
00878 method="stream.get",
00879 callback=self.async_callback(self._on_stream),
00880 session_key=self.current_user["session_key"])
00881
00882 def _on_stream(self, stream):
00883 if stream is None:
00884 # Not authorized to read the stream yet?
00885 self.redirect(self.authorize_redirect("read_stream"))
00886 return
00887 self.render("stream.html", stream=stream)
00888
00889 """
00890 self.require_setting("facebook_api_key", "Facebook Connect")
00891 self.require_setting("facebook_secret", "Facebook Connect")
00892 if not method.startswith("facebook."):
00893 method = "facebook." + method
00894 args["api_key"] = self.settings["facebook_api_key"]
00895 args["v"] = "1.0"
00896 args["method"] = method
00897 args["call_id"] = str(long(time.time() * 1e6))
00898 args["format"] = "json"
00899 args["sig"] = self._signature(args)
00900 url = "http://api.facebook.com/restserver.php?" + \
00901 urllib.urlencode(args)
00902 http = httpclient.AsyncHTTPClient()
00903 http.fetch(url, callback=self.async_callback(
00904 self._parse_response, callback))
00905
00906 def _on_get_user_info(self, callback, session, users):
00907 if users is None:
00908 callback(None)
00909 return
00910 callback({
00911 "name": users[0]["name"],
00912 "first_name": users[0]["first_name"],
00913 "last_name": users[0]["last_name"],
00914 "uid": users[0]["uid"],
00915 "locale": users[0]["locale"],
00916 "pic_square": users[0]["pic_square"],
00917 "profile_url": users[0]["profile_url"],
00918 "username": users[0].get("username"),
00919 "session_key": session["session_key"],
00920 "session_expires": session.get("expires"),
00921 })
00922
00923 def _parse_response(self, callback, response):
00924 if response.error:
00925 logging.warning("HTTP error from Facebook: %s", response.error)
00926 callback(None)
00927 return
00928 try:
00929 json = escape.json_decode(response.body)
00930 except Exception:
00931 logging.warning("Invalid JSON from Facebook: %r", response.body)
00932 callback(None)
00933 return
00934 if isinstance(json, dict) and json.get("error_code"):
00935 logging.warning("Facebook error: %d: %r", json["error_code"],
00936 json.get("error_msg"))
00937 callback(None)
00938 return
00939 callback(json)
00940
00941 def _signature(self, args):
00942 parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
00943 body = "".join(parts) + self.settings["facebook_secret"]
00944 if isinstance(body, unicode):
00945 body = body.encode("utf-8")
00946 return hashlib.md5(body).hexdigest()
00947
00948
00949 class FacebookGraphMixin(OAuth2Mixin):
00950 """Facebook authentication using the new Graph API and OAuth2."""
00951 _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
00952 _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
00953 _OAUTH_NO_CALLBACKS = False
00954
00955 def get_authenticated_user(self, redirect_uri, client_id, client_secret,
00956 code, callback, extra_fields=None):
00957 """Handles the login for the Facebook user, returning a user object.
00958
00959 Example usage::
00960
00961 class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin):
00962 @tornado.web.asynchronous
00963 def get(self):
00964 if self.get_argument("code", False):
00965 self.get_authenticated_user(
00966 redirect_uri='/auth/facebookgraph/',
00967 client_id=self.settings["facebook_api_key"],
00968 client_secret=self.settings["facebook_secret"],
00969 code=self.get_argument("code"),
00970 callback=self.async_callback(
00971 self._on_login))
00972 return
00973 self.authorize_redirect(redirect_uri='/auth/facebookgraph/',
00974 client_id=self.settings["facebook_api_key"],
00975 extra_params={"scope": "read_stream,offline_access"})
00976
00977 def _on_login(self, user):
00978 logging.error(user)
00979 self.finish()
00980
00981 """
00982 http = httpclient.AsyncHTTPClient()
00983 args = {
00984 "redirect_uri": redirect_uri,
00985 "code": code,
00986 "client_id": client_id,
00987 "client_secret": client_secret,
00988 }
00989
00990 fields = set(['id', 'name', 'first_name', 'last_name',
00991 'locale', 'picture', 'link'])
00992 if extra_fields:
00993 fields.update(extra_fields)
00994
00995 http.fetch(self._oauth_request_token_url(**args),
00996 self.async_callback(self._on_access_token, redirect_uri, client_id,
00997 client_secret, callback, fields))
00998
00999 def _on_access_token(self, redirect_uri, client_id, client_secret,
01000 callback, fields, response):
01001 if response.error:
01002 logging.warning('Facebook auth error: %s' % str(response))
01003 callback(None)
01004 return
01005
01006 args = escape.parse_qs_bytes(escape.native_str(response.body))
01007 session = {
01008 "access_token": args["access_token"][-1],
01009 "expires": args.get("expires")
01010 }
01011
01012 self.facebook_request(
01013 path="/me",
01014 callback=self.async_callback(
01015 self._on_get_user_info, callback, session, fields),
01016 access_token=session["access_token"],
01017 fields=",".join(fields)
01018 )
01019
01020 def _on_get_user_info(self, callback, session, fields, user):
01021 if user is None:
01022 callback(None)
01023 return
01024
01025 fieldmap = {}
01026 for field in fields:
01027 fieldmap[field] = user.get(field)
01028
01029 fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")})
01030 callback(fieldmap)
01031
01032 def facebook_request(self, path, callback, access_token=None,
01033 post_args=None, **args):
01034 """Fetches the given relative API path, e.g., "/btaylor/picture"
01035
01036 If the request is a POST, post_args should be provided. Query
01037 string arguments should be given as keyword arguments.
01038
01039 An introduction to the Facebook Graph API can be found at
01040 http://developers.facebook.com/docs/api
01041
01042 Many methods require an OAuth access token which you can obtain
01043 through authorize_redirect() and get_authenticated_user(). The
01044 user returned through that process includes an 'access_token'
01045 attribute that can be used to make authenticated requests via
01046 this method. Example usage::
01047
01048 class MainHandler(tornado.web.RequestHandler,
01049 tornado.auth.FacebookGraphMixin):
01050 @tornado.web.authenticated
01051 @tornado.web.asynchronous
01052 def get(self):
01053 self.facebook_request(
01054 "/me/feed",
01055 post_args={"message": "I am posting from my Tornado application!"},
01056 access_token=self.current_user["access_token"],
01057 callback=self.async_callback(self._on_post))
01058
01059 def _on_post(self, new_entry):
01060 if not new_entry:
01061 # Call failed; perhaps missing permission?
01062 self.authorize_redirect()
01063 return
01064 self.finish("Posted a message!")
01065
01066 """
01067 url = "https://graph.facebook.com" + path
01068 all_args = {}
01069 if access_token:
01070 all_args["access_token"] = access_token
01071 all_args.update(args)
01072
01073 if all_args:
01074 url += "?" + urllib.urlencode(all_args)
01075 callback = self.async_callback(self._on_facebook_request, callback)
01076 http = httpclient.AsyncHTTPClient()
01077 if post_args is not None:
01078 http.fetch(url, method="POST", body=urllib.urlencode(post_args),
01079 callback=callback)
01080 else:
01081 http.fetch(url, callback=callback)
01082
01083 def _on_facebook_request(self, callback, response):
01084 if response.error:
01085 logging.warning("Error response %s fetching %s", response.error,
01086 response.request.url)
01087 callback(None)
01088 return
01089 callback(escape.json_decode(response.body))
01090
01091
01092 def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
01093 """Calculates the HMAC-SHA1 OAuth signature for the given request.
01094
01095 See http://oauth.net/core/1.0/#signing_process
01096 """
01097 parts = urlparse.urlparse(url)
01098 scheme, netloc, path = parts[:3]
01099 normalized_url = scheme.lower() + "://" + netloc.lower() + path
01100
01101 base_elems = []
01102 base_elems.append(method.upper())
01103 base_elems.append(normalized_url)
01104 base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
01105 for k, v in sorted(parameters.items())))
01106 base_string = "&".join(_oauth_escape(e) for e in base_elems)
01107
01108 key_elems = [escape.utf8(consumer_token["secret"])]
01109 key_elems.append(escape.utf8(token["secret"] if token else ""))
01110 key = b("&").join(key_elems)
01111
01112 hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
01113 return binascii.b2a_base64(hash.digest())[:-1]
01114
01115
01116 def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
01117 """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
01118
01119 See http://oauth.net/core/1.0a/#signing_process
01120 """
01121 parts = urlparse.urlparse(url)
01122 scheme, netloc, path = parts[:3]
01123 normalized_url = scheme.lower() + "://" + netloc.lower() + path
01124
01125 base_elems = []
01126 base_elems.append(method.upper())
01127 base_elems.append(normalized_url)
01128 base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
01129 for k, v in sorted(parameters.items())))
01130
01131 base_string = "&".join(_oauth_escape(e) for e in base_elems)
01132 key_elems = [escape.utf8(urllib.quote(consumer_token["secret"], safe='~'))]
01133 key_elems.append(escape.utf8(urllib.quote(token["secret"], safe='~') if token else ""))
01134 key = b("&").join(key_elems)
01135
01136 hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
01137 return binascii.b2a_base64(hash.digest())[:-1]
01138
01139
01140 def _oauth_escape(val):
01141 if isinstance(val, unicode):
01142 val = val.encode("utf-8")
01143 return urllib.quote(val, safe="~")
01144
01145
01146 def _oauth_parse_response(body):
01147 p = escape.parse_qs(body, keep_blank_values=False)
01148 token = dict(key=p[b("oauth_token")][0], secret=p[b("oauth_token_secret")][0])
01149
01150
01151 special = (b("oauth_token"), b("oauth_token_secret"))
01152 token.update((k, p[k][0]) for k in p if k not in special)
01153 return token