import time
import uuid
-from tornado.concurrent import TracebackFuture, chain_future, return_future
+from tornado.concurrent import TracebackFuture, return_future
from tornado import gen
from tornado import httpclient
from tornado import escape
class OpenIdMixin(object):
"""Abstract implementation of OpenID and Attribute Exchange.
- See `GoogleMixin` below for a customized example (which also
- includes OAuth support).
-
Class attributes:
* ``_OPENID_ENDPOINT``: the identity provider's URI.
class OAuthMixin(object):
"""Abstract implementation of OAuth 1.0 and 1.0a.
- See `TwitterMixin` and `FriendFeedMixin` below for example implementations,
- or `GoogleMixin` for an OAuth/OpenID hybrid.
+ See `TwitterMixin` below for an example implementation.
Class attributes:
class OAuth2Mixin(object):
"""Abstract implementation of OAuth 2.0.
- See `FacebookGraphMixin` below for an example implementation.
+ See `FacebookGraphMixin` or `GoogleOAuth2Mixin` below for example
+ implementations.
Class attributes:
raise gen.Return(user)
-class FriendFeedMixin(OAuthMixin):
- """FriendFeed OAuth authentication.
-
- To authenticate with FriendFeed, register your application with
- FriendFeed at http://friendfeed.com/api/applications. Then copy
- your Consumer Key and Consumer Secret to the application
- `~tornado.web.Application.settings` ``friendfeed_consumer_key``
- and ``friendfeed_consumer_secret``. Use this mixin on the handler
- for the URL you registered as your application's Callback URL.
-
- When your application is set up, you can use this mixin like this
- to authenticate the user with FriendFeed and get access to their feed:
-
- .. testcode::
-
- class FriendFeedLoginHandler(tornado.web.RequestHandler,
- tornado.auth.FriendFeedMixin):
- @tornado.gen.coroutine
- def get(self):
- if self.get_argument("oauth_token", None):
- user = yield self.get_authenticated_user()
- # Save the user using e.g. set_secure_cookie()
- else:
- yield self.authorize_redirect()
-
- .. testoutput::
- :hide:
-
-
- The user object returned by `~OAuthMixin.get_authenticated_user()` includes the
- attributes ``username``, ``name``, and ``description`` in addition to
- ``access_token``. You should save the access token with the user;
- it is required to make requests on behalf of the user later with
- `friendfeed_request()`.
- """
- _OAUTH_VERSION = "1.0"
- _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
- _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
- _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
- _OAUTH_NO_CALLBACKS = True
- _OAUTH_VERSION = "1.0"
-
- @_auth_return_future
- def friendfeed_request(self, path, callback, access_token=None,
- post_args=None, **args):
- """Fetches the given relative API path, e.g., "/bret/friends"
-
- If the request is a POST, ``post_args`` should be provided. Query
- string arguments should be given as keyword arguments.
-
- All the FriendFeed methods are documented at
- http://friendfeed.com/api/documentation.
-
- Many methods require an OAuth access token which you can
- obtain through `~OAuthMixin.authorize_redirect` and
- `~OAuthMixin.get_authenticated_user`. The user returned
- through that process includes an ``access_token`` attribute that
- can be used to make authenticated requests via this
- method.
-
- Example usage:
-
- .. testcode::
-
- class MainHandler(tornado.web.RequestHandler,
- tornado.auth.FriendFeedMixin):
- @tornado.web.authenticated
- @tornado.gen.coroutine
- def get(self):
- new_entry = yield self.friendfeed_request(
- "/entry",
- post_args={"body": "Testing Tornado Web Server"},
- access_token=self.current_user["access_token"])
-
- if not new_entry:
- # Call failed; perhaps missing permission?
- yield self.authorize_redirect()
- return
- self.finish("Posted a message!")
-
- .. testoutput::
- :hide:
-
- """
- # Add the OAuth resource request signature if we have credentials
- url = "http://friendfeed-api.com/v2" + path
- if access_token:
- all_args = {}
- all_args.update(args)
- all_args.update(post_args or {})
- method = "POST" if post_args is not None else "GET"
- oauth = self._oauth_request_parameters(
- url, access_token, all_args, method=method)
- args.update(oauth)
- if args:
- url += "?" + urllib_parse.urlencode(args)
- callback = functools.partial(self._on_friendfeed_request, callback)
- http = self.get_auth_http_client()
- if post_args is not None:
- http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
- callback=callback)
- else:
- http.fetch(url, callback=callback)
-
- def _on_friendfeed_request(self, future, response):
- if response.error:
- future.set_exception(AuthError(
- "Error response %s fetching %s" % (response.error,
- response.request.url)))
- return
- future.set_result(escape.json_decode(response.body))
-
- def _oauth_consumer_token(self):
- self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
- self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
- return dict(
- key=self.settings["friendfeed_consumer_key"],
- secret=self.settings["friendfeed_consumer_secret"])
-
- @gen.coroutine
- def _oauth_get_user_future(self, access_token, callback):
- user = yield self.friendfeed_request(
- "/feedinfo/" + access_token["username"],
- include="id,name,description", access_token=access_token)
- if user:
- user["username"] = user["id"]
- callback(user)
-
- def _parse_user_response(self, callback, user):
- if user:
- user["username"] = user["id"]
- callback(user)
-
-
-class GoogleMixin(OpenIdMixin, OAuthMixin):
- """Google Open ID / OAuth authentication.
-
- .. deprecated:: 4.0
- New applications should use `GoogleOAuth2Mixin`
- below instead of this class. As of May 19, 2014, Google has stopped
- supporting registration-free authentication.
-
- No application registration is necessary to use Google for
- authentication or to access Google resources on behalf of a user.
-
- Google implements both OpenID and OAuth in a hybrid mode. If you
- just need the user's identity, use
- `~OpenIdMixin.authenticate_redirect`. If you need to make
- requests to Google on behalf of the user, use
- `authorize_redirect`. On return, parse the response with
- `~OpenIdMixin.get_authenticated_user`. We send a dict containing
- the values for the user, including ``email``, ``name``, and
- ``locale``.
-
- Example usage:
-
- .. testcode::
-
- class GoogleLoginHandler(tornado.web.RequestHandler,
- tornado.auth.GoogleMixin):
- @tornado.gen.coroutine
- def get(self):
- if self.get_argument("openid.mode", None):
- user = yield self.get_authenticated_user()
- # Save the user with e.g. set_secure_cookie()
- else:
- yield self.authenticate_redirect()
-
- .. testoutput::
- :hide:
-
- """
- _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
- _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
-
- @return_future
- def authorize_redirect(self, oauth_scope, callback_uri=None,
- ax_attrs=["name", "email", "language", "username"],
- callback=None):
- """Authenticates and authorizes for the given Google resource.
-
- Some of the available resources which can be used in the ``oauth_scope``
- argument are:
-
- * Gmail Contacts - http://www.google.com/m8/feeds/
- * Calendar - http://www.google.com/calendar/feeds/
- * Finance - http://finance.google.com/finance/feeds/
-
- You can authorize multiple resources by separating the resource
- URLs with a space.
-
- .. versionchanged:: 3.1
- Returns a `.Future` and takes an optional callback. These are
- not strictly necessary as this method is synchronous,
- but they are supplied for consistency with
- `OAuthMixin.authorize_redirect`.
- """
- callback_uri = callback_uri or self.request.uri
- args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
- oauth_scope=oauth_scope)
- self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
- callback()
-
- @_auth_return_future
- def get_authenticated_user(self, callback):
- """Fetches the authenticated user data upon redirect."""
- # Look to see if we are doing combined OpenID/OAuth
- oauth_ns = ""
- for name, values in self.request.arguments.items():
- if name.startswith("openid.ns.") and \
- values[-1] == b"http://specs.openid.net/extensions/oauth/1.0":
- oauth_ns = name[10:]
- break
- token = self.get_argument("openid." + oauth_ns + ".request_token", "")
- if token:
- http = self.get_auth_http_client()
- token = dict(key=token, secret="")
- http.fetch(self._oauth_access_token_url(token),
- functools.partial(self._on_access_token, callback))
- else:
- chain_future(OpenIdMixin.get_authenticated_user(self),
- callback)
-
- def _oauth_consumer_token(self):
- self.require_setting("google_consumer_key", "Google OAuth")
- self.require_setting("google_consumer_secret", "Google OAuth")
- return dict(
- key=self.settings["google_consumer_key"],
- secret=self.settings["google_consumer_secret"])
-
- def _oauth_get_user_future(self, access_token):
- return OpenIdMixin.get_authenticated_user(self)
-
-
class GoogleOAuth2Mixin(OAuth2Mixin):
"""Google authentication using OAuth2.
return httpclient.AsyncHTTPClient()
-class FacebookMixin(object):
- """Facebook Connect authentication.
-
- .. deprecated:: 1.1
- New applications should use `FacebookGraphMixin`
- below instead of this class. This class does not support the
- Future-based interface seen on other classes in this module.
-
- To authenticate with Facebook, register your application with
- Facebook at http://www.facebook.com/developers/apps.php. Then
- copy your API Key and Application Secret to the application settings
- ``facebook_api_key`` and ``facebook_secret``.
-
- When your application is set up, you can use this mixin like this
- to authenticate the user with Facebook:
-
- .. testcode::
-
- class FacebookHandler(tornado.web.RequestHandler,
- tornado.auth.FacebookMixin):
- @tornado.web.asynchronous
- def get(self):
- if self.get_argument("session", None):
- self.get_authenticated_user(self._on_auth)
- return
- yield self.authenticate_redirect()
-
- def _on_auth(self, user):
- if not user:
- raise tornado.web.HTTPError(500, "Facebook auth failed")
- # Save the user using, e.g., set_secure_cookie()
-
- .. testoutput::
- :hide:
-
- The user object returned by `get_authenticated_user` includes the
- attributes ``facebook_uid`` and ``name`` in addition to session attributes
- like ``session_key``. You should save the session key with the user; it is
- required to make requests on behalf of the user later with
- `facebook_request`.
- """
- @return_future
- def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
- extended_permissions=None, callback=None):
- """Authenticates/installs this app for the current user.
-
- .. versionchanged:: 3.1
- Returns a `.Future` and takes an optional callback. These are
- not strictly necessary as this method is synchronous,
- but they are supplied for consistency with
- `OAuthMixin.authorize_redirect`.
- """
- self.require_setting("facebook_api_key", "Facebook Connect")
- callback_uri = callback_uri or self.request.uri
- args = {
- "api_key": self.settings["facebook_api_key"],
- "v": "1.0",
- "fbconnect": "true",
- "display": "page",
- "next": urlparse.urljoin(self.request.full_url(), callback_uri),
- "return_session": "true",
- }
- if cancel_uri:
- args["cancel_url"] = urlparse.urljoin(
- self.request.full_url(), cancel_uri)
- if extended_permissions:
- if isinstance(extended_permissions, (unicode_type, bytes)):
- extended_permissions = [extended_permissions]
- args["req_perms"] = ",".join(extended_permissions)
- self.redirect("http://www.facebook.com/login.php?" +
- urllib_parse.urlencode(args))
- callback()
-
- def authorize_redirect(self, extended_permissions, callback_uri=None,
- cancel_uri=None, callback=None):
- """Redirects to an authorization request for the given FB resource.
-
- The available resource names are listed at
- http://wiki.developers.facebook.com/index.php/Extended_permission.
- The most common resource types include:
-
- * publish_stream
- * read_stream
- * email
- * sms
-
- extended_permissions can be a single permission name or a list of
- names. To get the session secret and session key, call
- get_authenticated_user() just as you would with
- authenticate_redirect().
-
- .. versionchanged:: 3.1
- Returns a `.Future` and takes an optional callback. These are
- not strictly necessary as this method is synchronous,
- but they are supplied for consistency with
- `OAuthMixin.authorize_redirect`.
- """
- return self.authenticate_redirect(callback_uri, cancel_uri,
- extended_permissions,
- callback=callback)
-
- def get_authenticated_user(self, callback):
- """Fetches the authenticated Facebook user.
-
- The authenticated user includes the special Facebook attributes
- 'session_key' and 'facebook_uid' in addition to the standard
- user attributes like 'name'.
- """
- self.require_setting("facebook_api_key", "Facebook Connect")
- session = escape.json_decode(self.get_argument("session"))
- self.facebook_request(
- method="facebook.users.getInfo",
- callback=functools.partial(
- self._on_get_user_info, callback, session),
- session_key=session["session_key"],
- uids=session["uid"],
- fields="uid,first_name,last_name,name,locale,pic_square,"
- "profile_url,username")
-
- def facebook_request(self, method, callback, **args):
- """Makes a Facebook API REST request.
-
- We automatically include the Facebook API key and signature, but
- it is the callers responsibility to include 'session_key' and any
- other required arguments to the method.
-
- The available Facebook methods are documented here:
- http://wiki.developers.facebook.com/index.php/API
-
- Here is an example for the stream.get() method:
-
- .. testcode::
-
- class MainHandler(tornado.web.RequestHandler,
- tornado.auth.FacebookMixin):
- @tornado.web.authenticated
- @tornado.web.asynchronous
- def get(self):
- self.facebook_request(
- method="stream.get",
- callback=self._on_stream,
- session_key=self.current_user["session_key"])
-
- def _on_stream(self, stream):
- if stream is None:
- # Not authorized to read the stream yet?
- self.redirect(self.authorize_redirect("read_stream"))
- return
- self.render("stream.html", stream=stream)
-
- .. testoutput::
- :hide:
-
- """
- self.require_setting("facebook_api_key", "Facebook Connect")
- self.require_setting("facebook_secret", "Facebook Connect")
- if not method.startswith("facebook."):
- method = "facebook." + method
- args["api_key"] = self.settings["facebook_api_key"]
- args["v"] = "1.0"
- args["method"] = method
- args["call_id"] = str(long(time.time() * 1e6))
- args["format"] = "json"
- args["sig"] = self._signature(args)
- url = "http://api.facebook.com/restserver.php?" + \
- urllib_parse.urlencode(args)
- http = self.get_auth_http_client()
- http.fetch(url, callback=functools.partial(
- self._parse_response, callback))
-
- def _on_get_user_info(self, callback, session, users):
- if users is None:
- callback(None)
- return
- callback({
- "name": users[0]["name"],
- "first_name": users[0]["first_name"],
- "last_name": users[0]["last_name"],
- "uid": users[0]["uid"],
- "locale": users[0]["locale"],
- "pic_square": users[0]["pic_square"],
- "profile_url": users[0]["profile_url"],
- "username": users[0].get("username"),
- "session_key": session["session_key"],
- "session_expires": session.get("expires"),
- })
-
- def _parse_response(self, callback, response):
- if response.error:
- gen_log.warning("HTTP error from Facebook: %s", response.error)
- callback(None)
- return
- try:
- json = escape.json_decode(response.body)
- except Exception:
- gen_log.warning("Invalid JSON from Facebook: %r", response.body)
- callback(None)
- return
- if isinstance(json, dict) and json.get("error_code"):
- gen_log.warning("Facebook error: %d: %r", json["error_code"],
- json.get("error_msg"))
- callback(None)
- return
- callback(json)
-
- def _signature(self, args):
- parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
- body = "".join(parts) + self.settings["facebook_secret"]
- if isinstance(body, unicode_type):
- 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."""
_OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"