auth.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2009 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
00016 
00017 """This module contains implementations of various third-party
00018 authentication schemes.
00019 
00020 All the classes in this file are class mixins designed to be used with
00021 the `tornado.web.RequestHandler` class.  They are used in two ways:
00022 
00023 * On a login handler, use methods such as ``authenticate_redirect()``,
00024   ``authorize_redirect()``, and ``get_authenticated_user()`` to
00025   establish the user's identity and store authentication tokens to your
00026   database and/or cookies.
00027 * In non-login handlers, use methods such as ``facebook_request()``
00028   or ``twitter_request()`` to use the authentication tokens to make
00029   requests to the respective services.
00030 
00031 They all take slightly different arguments due to the fact all these
00032 services implement authentication and authorization slightly differently.
00033 See the individual service classes below for complete documentation.
00034 
00035 Example usage for Google OpenID::
00036 
00037     class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
00038                                    tornado.auth.GoogleOAuth2Mixin):
00039         @tornado.gen.coroutine
00040         def get(self):
00041             if self.get_argument('code', False):
00042                 user = yield self.get_authenticated_user(
00043                     redirect_uri='http://your.site.com/auth/google',
00044                     code=self.get_argument('code'))
00045                 # Save the user with e.g. set_secure_cookie
00046             else:
00047                 yield self.authorize_redirect(
00048                     redirect_uri='http://your.site.com/auth/google',
00049                     client_id=self.settings['google_oauth']['key'],
00050                     scope=['profile', 'email'],
00051                     response_type='code',
00052                     extra_params={'approval_prompt': 'auto'})
00053 
00054 .. versionchanged:: 4.0
00055    All of the callback interfaces in this module are now guaranteed
00056    to run their callback with an argument of ``None`` on error.
00057    Previously some functions would do this while others would simply
00058    terminate the request on their own.  This change also ensures that
00059    errors are more consistently reported through the ``Future`` interfaces.
00060 """
00061 
00062 from __future__ import absolute_import, division, print_function, with_statement
00063 
00064 import base64
00065 import binascii
00066 import functools
00067 import hashlib
00068 import hmac
00069 import time
00070 import uuid
00071 
00072 from tornado.concurrent import TracebackFuture, chain_future, return_future
00073 from tornado import gen
00074 from tornado import httpclient
00075 from tornado import escape
00076 from tornado.httputil import url_concat
00077 from tornado.log import gen_log
00078 from tornado.stack_context import ExceptionStackContext
00079 from tornado.util import bytes_type, u, unicode_type, ArgReplacer
00080 
00081 try:
00082     import urlparse  # py2
00083 except ImportError:
00084     import urllib.parse as urlparse  # py3
00085 
00086 try:
00087     import urllib.parse as urllib_parse  # py3
00088 except ImportError:
00089     import urllib as urllib_parse  # py2
00090 
00091 try:
00092     long  # py2
00093 except NameError:
00094     long = int  # py3
00095 
00096 
00097 class AuthError(Exception):
00098     pass
00099 
00100 
00101 def _auth_future_to_callback(callback, future):
00102     try:
00103         result = future.result()
00104     except AuthError as e:
00105         gen_log.warning(str(e))
00106         result = None
00107     callback(result)
00108 
00109 
00110 def _auth_return_future(f):
00111     """Similar to tornado.concurrent.return_future, but uses the auth
00112     module's legacy callback interface.
00113 
00114     Note that when using this decorator the ``callback`` parameter
00115     inside the function will actually be a future.
00116     """
00117     replacer = ArgReplacer(f, 'callback')
00118 
00119     @functools.wraps(f)
00120     def wrapper(*args, **kwargs):
00121         future = TracebackFuture()
00122         callback, args, kwargs = replacer.replace(future, args, kwargs)
00123         if callback is not None:
00124             future.add_done_callback(
00125                 functools.partial(_auth_future_to_callback, callback))
00126         def handle_exception(typ, value, tb):
00127             if future.done():
00128                 return False
00129             else:
00130                 future.set_exc_info((typ, value, tb))
00131                 return True
00132         with ExceptionStackContext(handle_exception):
00133             f(*args, **kwargs)
00134         return future
00135     return wrapper
00136 
00137 
00138 class OpenIdMixin(object):
00139     """Abstract implementation of OpenID and Attribute Exchange.
00140 
00141     See `GoogleMixin` below for a customized example (which also
00142     includes OAuth support).
00143 
00144     Class attributes:
00145 
00146     * ``_OPENID_ENDPOINT``: the identity provider's URI.
00147     """
00148     @return_future
00149     def authenticate_redirect(self, callback_uri=None,
00150                               ax_attrs=["name", "email", "language", "username"],
00151                               callback=None):
00152         """Redirects to the authentication URL for this service.
00153 
00154         After authentication, the service will redirect back to the given
00155         callback URI with additional parameters including ``openid.mode``.
00156 
00157         We request the given attributes for the authenticated user by
00158         default (name, email, language, and username). If you don't need
00159         all those attributes for your app, you can request fewer with
00160         the ax_attrs keyword argument.
00161 
00162         .. versionchanged:: 3.1
00163            Returns a `.Future` and takes an optional callback.  These are
00164            not strictly necessary as this method is synchronous,
00165            but they are supplied for consistency with
00166            `OAuthMixin.authorize_redirect`.
00167         """
00168         callback_uri = callback_uri or self.request.uri
00169         args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
00170         self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
00171         callback()
00172 
00173     @_auth_return_future
00174     def get_authenticated_user(self, callback, http_client=None):
00175         """Fetches the authenticated user data upon redirect.
00176 
00177         This method should be called by the handler that receives the
00178         redirect from the `authenticate_redirect()` method (which is
00179         often the same as the one that calls it; in that case you would
00180         call `get_authenticated_user` if the ``openid.mode`` parameter
00181         is present and `authenticate_redirect` if it is not).
00182 
00183         The result of this method will generally be used to set a cookie.
00184         """
00185         # Verify the OpenID response via direct request to the OP
00186         args = dict((k, v[-1]) for k, v in self.request.arguments.items())
00187         args["openid.mode"] = u("check_authentication")
00188         url = self._OPENID_ENDPOINT
00189         if http_client is None:
00190             http_client = self.get_auth_http_client()
00191         http_client.fetch(url, functools.partial(
00192             self._on_authentication_verified, callback),
00193             method="POST", body=urllib_parse.urlencode(args))
00194 
00195     def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
00196         url = urlparse.urljoin(self.request.full_url(), callback_uri)
00197         args = {
00198             "openid.ns": "http://specs.openid.net/auth/2.0",
00199             "openid.claimed_id":
00200             "http://specs.openid.net/auth/2.0/identifier_select",
00201             "openid.identity":
00202             "http://specs.openid.net/auth/2.0/identifier_select",
00203             "openid.return_to": url,
00204             "openid.realm": urlparse.urljoin(url, '/'),
00205             "openid.mode": "checkid_setup",
00206         }
00207         if ax_attrs:
00208             args.update({
00209                 "openid.ns.ax": "http://openid.net/srv/ax/1.0",
00210                 "openid.ax.mode": "fetch_request",
00211             })
00212             ax_attrs = set(ax_attrs)
00213             required = []
00214             if "name" in ax_attrs:
00215                 ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
00216                 required += ["firstname", "fullname", "lastname"]
00217                 args.update({
00218                     "openid.ax.type.firstname":
00219                     "http://axschema.org/namePerson/first",
00220                     "openid.ax.type.fullname":
00221                     "http://axschema.org/namePerson",
00222                     "openid.ax.type.lastname":
00223                     "http://axschema.org/namePerson/last",
00224                 })
00225             known_attrs = {
00226                 "email": "http://axschema.org/contact/email",
00227                 "language": "http://axschema.org/pref/language",
00228                 "username": "http://axschema.org/namePerson/friendly",
00229             }
00230             for name in ax_attrs:
00231                 args["openid.ax.type." + name] = known_attrs[name]
00232                 required.append(name)
00233             args["openid.ax.required"] = ",".join(required)
00234         if oauth_scope:
00235             args.update({
00236                 "openid.ns.oauth":
00237                 "http://specs.openid.net/extensions/oauth/1.0",
00238                 "openid.oauth.consumer": self.request.host.split(":")[0],
00239                 "openid.oauth.scope": oauth_scope,
00240             })
00241         return args
00242 
00243     def _on_authentication_verified(self, future, response):
00244         if response.error or b"is_valid:true" not in response.body:
00245             future.set_exception(AuthError(
00246                 "Invalid OpenID response: %s" % (response.error or
00247                                                  response.body)))
00248             return
00249 
00250         # Make sure we got back at least an email from attribute exchange
00251         ax_ns = None
00252         for name in self.request.arguments:
00253             if name.startswith("openid.ns.") and \
00254                     self.get_argument(name) == u("http://openid.net/srv/ax/1.0"):
00255                 ax_ns = name[10:]
00256                 break
00257 
00258         def get_ax_arg(uri):
00259             if not ax_ns:
00260                 return u("")
00261             prefix = "openid." + ax_ns + ".type."
00262             ax_name = None
00263             for name in self.request.arguments.keys():
00264                 if self.get_argument(name) == uri and name.startswith(prefix):
00265                     part = name[len(prefix):]
00266                     ax_name = "openid." + ax_ns + ".value." + part
00267                     break
00268             if not ax_name:
00269                 return u("")
00270             return self.get_argument(ax_name, u(""))
00271 
00272         email = get_ax_arg("http://axschema.org/contact/email")
00273         name = get_ax_arg("http://axschema.org/namePerson")
00274         first_name = get_ax_arg("http://axschema.org/namePerson/first")
00275         last_name = get_ax_arg("http://axschema.org/namePerson/last")
00276         username = get_ax_arg("http://axschema.org/namePerson/friendly")
00277         locale = get_ax_arg("http://axschema.org/pref/language").lower()
00278         user = dict()
00279         name_parts = []
00280         if first_name:
00281             user["first_name"] = first_name
00282             name_parts.append(first_name)
00283         if last_name:
00284             user["last_name"] = last_name
00285             name_parts.append(last_name)
00286         if name:
00287             user["name"] = name
00288         elif name_parts:
00289             user["name"] = u(" ").join(name_parts)
00290         elif email:
00291             user["name"] = email.split("@")[0]
00292         if email:
00293             user["email"] = email
00294         if locale:
00295             user["locale"] = locale
00296         if username:
00297             user["username"] = username
00298         claimed_id = self.get_argument("openid.claimed_id", None)
00299         if claimed_id:
00300             user["claimed_id"] = claimed_id
00301         future.set_result(user)
00302 
00303     def get_auth_http_client(self):
00304         """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
00305 
00306         May be overridden by subclasses to use an HTTP client other than
00307         the default.
00308         """
00309         return httpclient.AsyncHTTPClient()
00310 
00311 
00312 class OAuthMixin(object):
00313     """Abstract implementation of OAuth 1.0 and 1.0a.
00314 
00315     See `TwitterMixin` and `FriendFeedMixin` below for example implementations,
00316     or `GoogleMixin` for an OAuth/OpenID hybrid.
00317 
00318     Class attributes:
00319 
00320     * ``_OAUTH_AUTHORIZE_URL``: The service's OAuth authorization url.
00321     * ``_OAUTH_ACCESS_TOKEN_URL``: The service's OAuth access token url.
00322     * ``_OAUTH_VERSION``: May be either "1.0" or "1.0a".
00323     * ``_OAUTH_NO_CALLBACKS``: Set this to True if the service requires
00324       advance registration of callbacks.
00325 
00326     Subclasses must also override the `_oauth_get_user_future` and
00327     `_oauth_consumer_token` methods.
00328     """
00329     @return_future
00330     def authorize_redirect(self, callback_uri=None, extra_params=None,
00331                            http_client=None, callback=None):
00332         """Redirects the user to obtain OAuth authorization for this service.
00333 
00334         The ``callback_uri`` may be omitted if you have previously
00335         registered a callback URI with the third-party service.  For
00336         some sevices (including Friendfeed), you must use a
00337         previously-registered callback URI and cannot specify a
00338         callback via this method.
00339 
00340         This method sets a cookie called ``_oauth_request_token`` which is
00341         subsequently used (and cleared) in `get_authenticated_user` for
00342         security purposes.
00343 
00344         Note that this method is asynchronous, although it calls
00345         `.RequestHandler.finish` for you so it may not be necessary
00346         to pass a callback or use the `.Future` it returns.  However,
00347         if this method is called from a function decorated with
00348         `.gen.coroutine`, you must call it with ``yield`` to keep the
00349         response from being closed prematurely.
00350 
00351         .. versionchanged:: 3.1
00352            Now returns a `.Future` and takes an optional callback, for
00353            compatibility with `.gen.coroutine`.
00354         """
00355         if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
00356             raise Exception("This service does not support oauth_callback")
00357         if http_client is None:
00358             http_client = self.get_auth_http_client()
00359         if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00360             http_client.fetch(
00361                 self._oauth_request_token_url(callback_uri=callback_uri,
00362                                               extra_params=extra_params),
00363                 functools.partial(
00364                     self._on_request_token,
00365                     self._OAUTH_AUTHORIZE_URL,
00366                     callback_uri,
00367                     callback))
00368         else:
00369             http_client.fetch(
00370                 self._oauth_request_token_url(),
00371                 functools.partial(
00372                     self._on_request_token, self._OAUTH_AUTHORIZE_URL,
00373                     callback_uri,
00374                     callback))
00375 
00376     @_auth_return_future
00377     def get_authenticated_user(self, callback, http_client=None):
00378         """Gets the OAuth authorized user and access token.
00379 
00380         This method should be called from the handler for your
00381         OAuth callback URL to complete the registration process. We run the
00382         callback with the authenticated user dictionary.  This dictionary
00383         will contain an ``access_key`` which can be used to make authorized
00384         requests to this service on behalf of the user.  The dictionary will
00385         also contain other fields such as ``name``, depending on the service
00386         used.
00387         """
00388         future = callback
00389         request_key = escape.utf8(self.get_argument("oauth_token"))
00390         oauth_verifier = self.get_argument("oauth_verifier", None)
00391         request_cookie = self.get_cookie("_oauth_request_token")
00392         if not request_cookie:
00393             future.set_exception(AuthError(
00394                 "Missing OAuth request token cookie"))
00395             return
00396         self.clear_cookie("_oauth_request_token")
00397         cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")]
00398         if cookie_key != request_key:
00399             future.set_exception(AuthError(
00400                 "Request token does not match cookie"))
00401             return
00402         token = dict(key=cookie_key, secret=cookie_secret)
00403         if oauth_verifier:
00404             token["verifier"] = oauth_verifier
00405         if http_client is None:
00406             http_client = self.get_auth_http_client()
00407         http_client.fetch(self._oauth_access_token_url(token),
00408                           functools.partial(self._on_access_token, callback))
00409 
00410     def _oauth_request_token_url(self, callback_uri=None, extra_params=None):
00411         consumer_token = self._oauth_consumer_token()
00412         url = self._OAUTH_REQUEST_TOKEN_URL
00413         args = dict(
00414             oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
00415             oauth_signature_method="HMAC-SHA1",
00416             oauth_timestamp=str(int(time.time())),
00417             oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
00418             oauth_version="1.0",
00419         )
00420         if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00421             if callback_uri == "oob":
00422                 args["oauth_callback"] = "oob"
00423             elif callback_uri:
00424                 args["oauth_callback"] = urlparse.urljoin(
00425                     self.request.full_url(), callback_uri)
00426             if extra_params:
00427                 args.update(extra_params)
00428             signature = _oauth10a_signature(consumer_token, "GET", url, args)
00429         else:
00430             signature = _oauth_signature(consumer_token, "GET", url, args)
00431 
00432         args["oauth_signature"] = signature
00433         return url + "?" + urllib_parse.urlencode(args)
00434 
00435     def _on_request_token(self, authorize_url, callback_uri, callback,
00436                           response):
00437         if response.error:
00438             raise Exception("Could not get request token: %s" % response.error)
00439         request_token = _oauth_parse_response(response.body)
00440         data = (base64.b64encode(escape.utf8(request_token["key"])) + b"|" +
00441                 base64.b64encode(escape.utf8(request_token["secret"])))
00442         self.set_cookie("_oauth_request_token", data)
00443         args = dict(oauth_token=request_token["key"])
00444         if callback_uri == "oob":
00445             self.finish(authorize_url + "?" + urllib_parse.urlencode(args))
00446             callback()
00447             return
00448         elif callback_uri:
00449             args["oauth_callback"] = urlparse.urljoin(
00450                 self.request.full_url(), callback_uri)
00451         self.redirect(authorize_url + "?" + urllib_parse.urlencode(args))
00452         callback()
00453 
00454     def _oauth_access_token_url(self, request_token):
00455         consumer_token = self._oauth_consumer_token()
00456         url = self._OAUTH_ACCESS_TOKEN_URL
00457         args = dict(
00458             oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
00459             oauth_token=escape.to_basestring(request_token["key"]),
00460             oauth_signature_method="HMAC-SHA1",
00461             oauth_timestamp=str(int(time.time())),
00462             oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
00463             oauth_version="1.0",
00464         )
00465         if "verifier" in request_token:
00466             args["oauth_verifier"] = request_token["verifier"]
00467 
00468         if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00469             signature = _oauth10a_signature(consumer_token, "GET", url, args,
00470                                             request_token)
00471         else:
00472             signature = _oauth_signature(consumer_token, "GET", url, args,
00473                                          request_token)
00474 
00475         args["oauth_signature"] = signature
00476         return url + "?" + urllib_parse.urlencode(args)
00477 
00478     def _on_access_token(self, future, response):
00479         if response.error:
00480             future.set_exception(AuthError("Could not fetch access token"))
00481             return
00482 
00483         access_token = _oauth_parse_response(response.body)
00484         self._oauth_get_user_future(access_token).add_done_callback(
00485             functools.partial(self._on_oauth_get_user, access_token, future))
00486 
00487     def _oauth_consumer_token(self):
00488         """Subclasses must override this to return their OAuth consumer keys.
00489 
00490         The return value should be a `dict` with keys ``key`` and ``secret``.
00491         """
00492         raise NotImplementedError()
00493 
00494     @return_future
00495     def _oauth_get_user_future(self, access_token, callback):
00496         """Subclasses must override this to get basic information about the
00497         user.
00498 
00499         Should return a `.Future` whose result is a dictionary
00500         containing information about the user, which may have been
00501         retrieved by using ``access_token`` to make a request to the
00502         service.
00503 
00504         The access token will be added to the returned dictionary to make
00505         the result of `get_authenticated_user`.
00506 
00507         For backwards compatibility, the callback-based ``_oauth_get_user``
00508         method is also supported.
00509         """
00510         # By default, call the old-style _oauth_get_user, but new code
00511         # should override this method instead.
00512         self._oauth_get_user(access_token, callback)
00513 
00514     def _oauth_get_user(self, access_token, callback):
00515         raise NotImplementedError()
00516 
00517     def _on_oauth_get_user(self, access_token, future, user_future):
00518         if user_future.exception() is not None:
00519             future.set_exception(user_future.exception())
00520             return
00521         user = user_future.result()
00522         if not user:
00523             future.set_exception(AuthError("Error getting user"))
00524             return
00525         user["access_token"] = access_token
00526         future.set_result(user)
00527 
00528     def _oauth_request_parameters(self, url, access_token, parameters={},
00529                                   method="GET"):
00530         """Returns the OAuth parameters as a dict for the given request.
00531 
00532         parameters should include all POST arguments and query string arguments
00533         that will be sent with the request.
00534         """
00535         consumer_token = self._oauth_consumer_token()
00536         base_args = dict(
00537             oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
00538             oauth_token=escape.to_basestring(access_token["key"]),
00539             oauth_signature_method="HMAC-SHA1",
00540             oauth_timestamp=str(int(time.time())),
00541             oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
00542             oauth_version="1.0",
00543         )
00544         args = {}
00545         args.update(base_args)
00546         args.update(parameters)
00547         if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
00548             signature = _oauth10a_signature(consumer_token, method, url, args,
00549                                             access_token)
00550         else:
00551             signature = _oauth_signature(consumer_token, method, url, args,
00552                                          access_token)
00553         base_args["oauth_signature"] = escape.to_basestring(signature)
00554         return base_args
00555 
00556     def get_auth_http_client(self):
00557         """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
00558 
00559         May be overridden by subclasses to use an HTTP client other than
00560         the default.
00561         """
00562         return httpclient.AsyncHTTPClient()
00563 
00564 
00565 class OAuth2Mixin(object):
00566     """Abstract implementation of OAuth 2.0.
00567 
00568     See `FacebookGraphMixin` below for an example implementation.
00569 
00570     Class attributes:
00571 
00572     * ``_OAUTH_AUTHORIZE_URL``: The service's authorization url.
00573     * ``_OAUTH_ACCESS_TOKEN_URL``:  The service's access token url.
00574     """
00575     @return_future
00576     def authorize_redirect(self, redirect_uri=None, client_id=None,
00577                            client_secret=None, extra_params=None,
00578                            callback=None, scope=None, response_type="code"):
00579         """Redirects the user to obtain OAuth authorization for this service.
00580 
00581         Some providers require that you register a redirect URL with
00582         your application instead of passing one via this method. You
00583         should call this method to log the user in, and then call
00584         ``get_authenticated_user`` in the handler for your
00585         redirect URL to complete the authorization process.
00586 
00587         .. versionchanged:: 3.1
00588            Returns a `.Future` and takes an optional callback.  These are
00589            not strictly necessary as this method is synchronous,
00590            but they are supplied for consistency with
00591            `OAuthMixin.authorize_redirect`.
00592         """
00593         args = {
00594             "redirect_uri": redirect_uri,
00595             "client_id": client_id,
00596             "response_type": response_type
00597         }
00598         if extra_params:
00599             args.update(extra_params)
00600         if scope:
00601             args['scope'] = ' '.join(scope)
00602         self.redirect(
00603             url_concat(self._OAUTH_AUTHORIZE_URL, args))
00604         callback()
00605 
00606     def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
00607                                  client_secret=None, code=None,
00608                                  extra_params=None):
00609         url = self._OAUTH_ACCESS_TOKEN_URL
00610         args = dict(
00611             redirect_uri=redirect_uri,
00612             code=code,
00613             client_id=client_id,
00614             client_secret=client_secret,
00615         )
00616         if extra_params:
00617             args.update(extra_params)
00618         return url_concat(url, args)
00619 
00620 
00621 class TwitterMixin(OAuthMixin):
00622     """Twitter OAuth authentication.
00623 
00624     To authenticate with Twitter, register your application with
00625     Twitter at http://twitter.com/apps. Then copy your Consumer Key
00626     and Consumer Secret to the application
00627     `~tornado.web.Application.settings` ``twitter_consumer_key`` and
00628     ``twitter_consumer_secret``. Use this mixin on the handler for the
00629     URL you registered as your application's callback URL.
00630 
00631     When your application is set up, you can use this mixin like this
00632     to authenticate the user with Twitter and get access to their stream::
00633 
00634         class TwitterLoginHandler(tornado.web.RequestHandler,
00635                                   tornado.auth.TwitterMixin):
00636             @tornado.gen.coroutine
00637             def get(self):
00638                 if self.get_argument("oauth_token", None):
00639                     user = yield self.get_authenticated_user()
00640                     # Save the user using e.g. set_secure_cookie()
00641                 else:
00642                     yield self.authorize_redirect()
00643 
00644     The user object returned by `~OAuthMixin.get_authenticated_user`
00645     includes the attributes ``username``, ``name``, ``access_token``,
00646     and all of the custom Twitter user attributes described at
00647     https://dev.twitter.com/docs/api/1.1/get/users/show
00648     """
00649     _OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"
00650     _OAUTH_ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token"
00651     _OAUTH_AUTHORIZE_URL = "https://api.twitter.com/oauth/authorize"
00652     _OAUTH_AUTHENTICATE_URL = "https://api.twitter.com/oauth/authenticate"
00653     _OAUTH_NO_CALLBACKS = False
00654     _TWITTER_BASE_URL = "https://api.twitter.com/1.1"
00655 
00656     @return_future
00657     def authenticate_redirect(self, callback_uri=None, callback=None):
00658         """Just like `~OAuthMixin.authorize_redirect`, but
00659         auto-redirects if authorized.
00660 
00661         This is generally the right interface to use if you are using
00662         Twitter for single-sign on.
00663 
00664         .. versionchanged:: 3.1
00665            Now returns a `.Future` and takes an optional callback, for
00666            compatibility with `.gen.coroutine`.
00667         """
00668         http = self.get_auth_http_client()
00669         http.fetch(self._oauth_request_token_url(callback_uri=callback_uri),
00670                    functools.partial(
00671                        self._on_request_token, self._OAUTH_AUTHENTICATE_URL,
00672                        None, callback))
00673 
00674     @_auth_return_future
00675     def twitter_request(self, path, callback=None, access_token=None,
00676                         post_args=None, **args):
00677         """Fetches the given API path, e.g., ``statuses/user_timeline/btaylor``
00678 
00679         The path should not include the format or API version number.
00680         (we automatically use JSON format and API version 1).
00681 
00682         If the request is a POST, ``post_args`` should be provided. Query
00683         string arguments should be given as keyword arguments.
00684 
00685         All the Twitter methods are documented at http://dev.twitter.com/
00686 
00687         Many methods require an OAuth access token which you can
00688         obtain through `~OAuthMixin.authorize_redirect` and
00689         `~OAuthMixin.get_authenticated_user`. The user returned through that
00690         process includes an 'access_token' attribute that can be used
00691         to make authenticated requests via this method. Example
00692         usage::
00693 
00694             class MainHandler(tornado.web.RequestHandler,
00695                               tornado.auth.TwitterMixin):
00696                 @tornado.web.authenticated
00697                 @tornado.gen.coroutine
00698                 def get(self):
00699                     new_entry = yield self.twitter_request(
00700                         "/statuses/update",
00701                         post_args={"status": "Testing Tornado Web Server"},
00702                         access_token=self.current_user["access_token"])
00703                     if not new_entry:
00704                         # Call failed; perhaps missing permission?
00705                         yield self.authorize_redirect()
00706                         return
00707                     self.finish("Posted a message!")
00708 
00709         """
00710         if path.startswith('http:') or path.startswith('https:'):
00711             # Raw urls are useful for e.g. search which doesn't follow the
00712             # usual pattern: http://search.twitter.com/search.json
00713             url = path
00714         else:
00715             url = self._TWITTER_BASE_URL + path + ".json"
00716         # Add the OAuth resource request signature if we have credentials
00717         if access_token:
00718             all_args = {}
00719             all_args.update(args)
00720             all_args.update(post_args or {})
00721             method = "POST" if post_args is not None else "GET"
00722             oauth = self._oauth_request_parameters(
00723                 url, access_token, all_args, method=method)
00724             args.update(oauth)
00725         if args:
00726             url += "?" + urllib_parse.urlencode(args)
00727         http = self.get_auth_http_client()
00728         http_callback = functools.partial(self._on_twitter_request, callback)
00729         if post_args is not None:
00730             http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
00731                        callback=http_callback)
00732         else:
00733             http.fetch(url, callback=http_callback)
00734 
00735     def _on_twitter_request(self, future, response):
00736         if response.error:
00737             future.set_exception(AuthError(
00738                 "Error response %s fetching %s" % (response.error,
00739                                                    response.request.url)))
00740             return
00741         future.set_result(escape.json_decode(response.body))
00742 
00743     def _oauth_consumer_token(self):
00744         self.require_setting("twitter_consumer_key", "Twitter OAuth")
00745         self.require_setting("twitter_consumer_secret", "Twitter OAuth")
00746         return dict(
00747             key=self.settings["twitter_consumer_key"],
00748             secret=self.settings["twitter_consumer_secret"])
00749 
00750     @gen.coroutine
00751     def _oauth_get_user_future(self, access_token):
00752         user = yield self.twitter_request(
00753             "/account/verify_credentials",
00754             access_token=access_token)
00755         if user:
00756             user["username"] = user["screen_name"]
00757         raise gen.Return(user)
00758 
00759 
00760 class FriendFeedMixin(OAuthMixin):
00761     """FriendFeed OAuth authentication.
00762 
00763     To authenticate with FriendFeed, register your application with
00764     FriendFeed at http://friendfeed.com/api/applications. Then copy
00765     your Consumer Key and Consumer Secret to the application
00766     `~tornado.web.Application.settings` ``friendfeed_consumer_key``
00767     and ``friendfeed_consumer_secret``. Use this mixin on the handler
00768     for the URL you registered as your application's Callback URL.
00769 
00770     When your application is set up, you can use this mixin like this
00771     to authenticate the user with FriendFeed and get access to their feed::
00772 
00773         class FriendFeedLoginHandler(tornado.web.RequestHandler,
00774                                      tornado.auth.FriendFeedMixin):
00775             @tornado.gen.coroutine
00776             def get(self):
00777                 if self.get_argument("oauth_token", None):
00778                     user = yield self.get_authenticated_user()
00779                     # Save the user using e.g. set_secure_cookie()
00780                 else:
00781                     yield self.authorize_redirect()
00782 
00783     The user object returned by `~OAuthMixin.get_authenticated_user()` includes the
00784     attributes ``username``, ``name``, and ``description`` in addition to
00785     ``access_token``. You should save the access token with the user;
00786     it is required to make requests on behalf of the user later with
00787     `friendfeed_request()`.
00788     """
00789     _OAUTH_VERSION = "1.0"
00790     _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
00791     _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
00792     _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
00793     _OAUTH_NO_CALLBACKS = True
00794     _OAUTH_VERSION = "1.0"
00795 
00796     @_auth_return_future
00797     def friendfeed_request(self, path, callback, access_token=None,
00798                            post_args=None, **args):
00799         """Fetches the given relative API path, e.g., "/bret/friends"
00800 
00801         If the request is a POST, ``post_args`` should be provided. Query
00802         string arguments should be given as keyword arguments.
00803 
00804         All the FriendFeed methods are documented at
00805         http://friendfeed.com/api/documentation.
00806 
00807         Many methods require an OAuth access token which you can
00808         obtain through `~OAuthMixin.authorize_redirect` and
00809         `~OAuthMixin.get_authenticated_user`. The user returned
00810         through that process includes an ``access_token`` attribute that
00811         can be used to make authenticated requests via this
00812         method.
00813 
00814         Example usage::
00815 
00816             class MainHandler(tornado.web.RequestHandler,
00817                               tornado.auth.FriendFeedMixin):
00818                 @tornado.web.authenticated
00819                 @tornado.gen.coroutine
00820                 def get(self):
00821                     new_entry = yield self.friendfeed_request(
00822                         "/entry",
00823                         post_args={"body": "Testing Tornado Web Server"},
00824                         access_token=self.current_user["access_token"])
00825 
00826                     if not new_entry:
00827                         # Call failed; perhaps missing permission?
00828                         yield self.authorize_redirect()
00829                         return
00830                     self.finish("Posted a message!")
00831 
00832         """
00833         # Add the OAuth resource request signature if we have credentials
00834         url = "http://friendfeed-api.com/v2" + path
00835         if access_token:
00836             all_args = {}
00837             all_args.update(args)
00838             all_args.update(post_args or {})
00839             method = "POST" if post_args is not None else "GET"
00840             oauth = self._oauth_request_parameters(
00841                 url, access_token, all_args, method=method)
00842             args.update(oauth)
00843         if args:
00844             url += "?" + urllib_parse.urlencode(args)
00845         callback = functools.partial(self._on_friendfeed_request, callback)
00846         http = self.get_auth_http_client()
00847         if post_args is not None:
00848             http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
00849                        callback=callback)
00850         else:
00851             http.fetch(url, callback=callback)
00852 
00853     def _on_friendfeed_request(self, future, response):
00854         if response.error:
00855             future.set_exception(AuthError(
00856                 "Error response %s fetching %s" % (response.error,
00857                                                    response.request.url)))
00858             return
00859         future.set_result(escape.json_decode(response.body))
00860 
00861     def _oauth_consumer_token(self):
00862         self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
00863         self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
00864         return dict(
00865             key=self.settings["friendfeed_consumer_key"],
00866             secret=self.settings["friendfeed_consumer_secret"])
00867 
00868     @gen.coroutine
00869     def _oauth_get_user_future(self, access_token, callback):
00870         user = yield self.friendfeed_request(
00871             "/feedinfo/" + access_token["username"],
00872             include="id,name,description", access_token=access_token)
00873         if user:
00874             user["username"] = user["id"]
00875         callback(user)
00876 
00877     def _parse_user_response(self, callback, user):
00878         if user:
00879             user["username"] = user["id"]
00880         callback(user)
00881 
00882 
00883 class GoogleMixin(OpenIdMixin, OAuthMixin):
00884     """Google Open ID / OAuth authentication.
00885 
00886     .. deprecated:: 4.0
00887        New applications should use `GoogleOAuth2Mixin`
00888        below instead of this class. As of May 19, 2014, Google has stopped
00889        supporting registration-free authentication.
00890 
00891     No application registration is necessary to use Google for
00892     authentication or to access Google resources on behalf of a user.
00893 
00894     Google implements both OpenID and OAuth in a hybrid mode.  If you
00895     just need the user's identity, use
00896     `~OpenIdMixin.authenticate_redirect`.  If you need to make
00897     requests to Google on behalf of the user, use
00898     `authorize_redirect`.  On return, parse the response with
00899     `~OpenIdMixin.get_authenticated_user`. We send a dict containing
00900     the values for the user, including ``email``, ``name``, and
00901     ``locale``.
00902 
00903     Example usage::
00904 
00905         class GoogleLoginHandler(tornado.web.RequestHandler,
00906                                  tornado.auth.GoogleMixin):
00907            @tornado.gen.coroutine
00908            def get(self):
00909                if self.get_argument("openid.mode", None):
00910                    user = yield self.get_authenticated_user()
00911                    # Save the user with e.g. set_secure_cookie()
00912                else:
00913                    yield self.authenticate_redirect()
00914     """
00915     _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
00916     _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
00917 
00918     @return_future
00919     def authorize_redirect(self, oauth_scope, callback_uri=None,
00920                            ax_attrs=["name", "email", "language", "username"],
00921                            callback=None):
00922         """Authenticates and authorizes for the given Google resource.
00923 
00924         Some of the available resources which can be used in the ``oauth_scope``
00925         argument are:
00926 
00927         * Gmail Contacts - http://www.google.com/m8/feeds/
00928         * Calendar - http://www.google.com/calendar/feeds/
00929         * Finance - http://finance.google.com/finance/feeds/
00930 
00931         You can authorize multiple resources by separating the resource
00932         URLs with a space.
00933 
00934         .. versionchanged:: 3.1
00935            Returns a `.Future` and takes an optional callback.  These are
00936            not strictly necessary as this method is synchronous,
00937            but they are supplied for consistency with
00938            `OAuthMixin.authorize_redirect`.
00939         """
00940         callback_uri = callback_uri or self.request.uri
00941         args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
00942                                  oauth_scope=oauth_scope)
00943         self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
00944         callback()
00945 
00946     @_auth_return_future
00947     def get_authenticated_user(self, callback):
00948         """Fetches the authenticated user data upon redirect."""
00949         # Look to see if we are doing combined OpenID/OAuth
00950         oauth_ns = ""
00951         for name, values in self.request.arguments.items():
00952             if name.startswith("openid.ns.") and \
00953                     values[-1] == b"http://specs.openid.net/extensions/oauth/1.0":
00954                 oauth_ns = name[10:]
00955                 break
00956         token = self.get_argument("openid." + oauth_ns + ".request_token", "")
00957         if token:
00958             http = self.get_auth_http_client()
00959             token = dict(key=token, secret="")
00960             http.fetch(self._oauth_access_token_url(token),
00961                        functools.partial(self._on_access_token, callback))
00962         else:
00963             chain_future(OpenIdMixin.get_authenticated_user(self),
00964                          callback)
00965 
00966     def _oauth_consumer_token(self):
00967         self.require_setting("google_consumer_key", "Google OAuth")
00968         self.require_setting("google_consumer_secret", "Google OAuth")
00969         return dict(
00970             key=self.settings["google_consumer_key"],
00971             secret=self.settings["google_consumer_secret"])
00972 
00973     def _oauth_get_user_future(self, access_token):
00974         return OpenIdMixin.get_authenticated_user(self)
00975 
00976 
00977 class GoogleOAuth2Mixin(OAuth2Mixin):
00978     """Google authentication using OAuth2.
00979 
00980     In order to use, register your application with Google and copy the
00981     relevant parameters to your application settings.
00982 
00983     * Go to the Google Dev Console at http://console.developers.google.com
00984     * Select a project, or create a new one.
00985     * In the sidebar on the left, select APIs & Auth.
00986     * In the list of APIs, find the Google+ API service and set it to ON.
00987     * In the sidebar on the left, select Credentials.
00988     * In the OAuth section of the page, select Create New Client ID.
00989     * Set the Redirect URI to point to your auth handler
00990     * Copy the "Client secret" and "Client ID" to the application settings as
00991       {"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}
00992 
00993     .. versionadded:: 3.2
00994     """
00995     _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth"
00996     _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"
00997     _OAUTH_NO_CALLBACKS = False
00998     _OAUTH_SETTINGS_KEY = 'google_oauth'
00999 
01000     @_auth_return_future
01001     def get_authenticated_user(self, redirect_uri, code, callback):
01002         """Handles the login for the Google user, returning a user object.
01003 
01004         Example usage::
01005 
01006             class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
01007                                            tornado.auth.GoogleOAuth2Mixin):
01008                 @tornado.gen.coroutine
01009                 def get(self):
01010                     if self.get_argument('code', False):
01011                         user = yield self.get_authenticated_user(
01012                             redirect_uri='http://your.site.com/auth/google',
01013                             code=self.get_argument('code'))
01014                         # Save the user with e.g. set_secure_cookie
01015                     else:
01016                         yield self.authorize_redirect(
01017                             redirect_uri='http://your.site.com/auth/google',
01018                             client_id=self.settings['google_oauth']['key'],
01019                             scope=['profile', 'email'],
01020                             response_type='code',
01021                             extra_params={'approval_prompt': 'auto'})
01022         """
01023         http = self.get_auth_http_client()
01024         body = urllib_parse.urlencode({
01025             "redirect_uri": redirect_uri,
01026             "code": code,
01027             "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'],
01028             "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'],
01029             "grant_type": "authorization_code",
01030         })
01031 
01032         http.fetch(self._OAUTH_ACCESS_TOKEN_URL,
01033                    functools.partial(self._on_access_token, callback),
01034                    method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body)
01035 
01036     def _on_access_token(self, future, response):
01037         """Callback function for the exchange to the access token."""
01038         if response.error:
01039             future.set_exception(AuthError('Google auth error: %s' % str(response)))
01040             return
01041 
01042         args = escape.json_decode(response.body)
01043         future.set_result(args)
01044 
01045     def get_auth_http_client(self):
01046         """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
01047 
01048         May be overridden by subclasses to use an HTTP client other than
01049         the default.
01050         """
01051         return httpclient.AsyncHTTPClient()
01052 
01053 
01054 class FacebookMixin(object):
01055     """Facebook Connect authentication.
01056 
01057     .. deprecated:: 1.1
01058        New applications should use `FacebookGraphMixin`
01059        below instead of this class.  This class does not support the
01060        Future-based interface seen on other classes in this module.
01061 
01062     To authenticate with Facebook, register your application with
01063     Facebook at http://www.facebook.com/developers/apps.php. Then
01064     copy your API Key and Application Secret to the application settings
01065     ``facebook_api_key`` and ``facebook_secret``.
01066 
01067     When your application is set up, you can use this mixin like this
01068     to authenticate the user with Facebook::
01069 
01070         class FacebookHandler(tornado.web.RequestHandler,
01071                               tornado.auth.FacebookMixin):
01072             @tornado.web.asynchronous
01073             def get(self):
01074                 if self.get_argument("session", None):
01075                     self.get_authenticated_user(self._on_auth)
01076                     return
01077                 yield self.authenticate_redirect()
01078 
01079             def _on_auth(self, user):
01080                 if not user:
01081                     raise tornado.web.HTTPError(500, "Facebook auth failed")
01082                 # Save the user using, e.g., set_secure_cookie()
01083 
01084     The user object returned by `get_authenticated_user` includes the
01085     attributes ``facebook_uid`` and ``name`` in addition to session attributes
01086     like ``session_key``. You should save the session key with the user; it is
01087     required to make requests on behalf of the user later with
01088     `facebook_request`.
01089     """
01090     @return_future
01091     def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
01092                               extended_permissions=None, callback=None):
01093         """Authenticates/installs this app for the current user.
01094 
01095         .. versionchanged:: 3.1
01096            Returns a `.Future` and takes an optional callback.  These are
01097            not strictly necessary as this method is synchronous,
01098            but they are supplied for consistency with
01099            `OAuthMixin.authorize_redirect`.
01100         """
01101         self.require_setting("facebook_api_key", "Facebook Connect")
01102         callback_uri = callback_uri or self.request.uri
01103         args = {
01104             "api_key": self.settings["facebook_api_key"],
01105             "v": "1.0",
01106             "fbconnect": "true",
01107             "display": "page",
01108             "next": urlparse.urljoin(self.request.full_url(), callback_uri),
01109             "return_session": "true",
01110         }
01111         if cancel_uri:
01112             args["cancel_url"] = urlparse.urljoin(
01113                 self.request.full_url(), cancel_uri)
01114         if extended_permissions:
01115             if isinstance(extended_permissions, (unicode_type, bytes_type)):
01116                 extended_permissions = [extended_permissions]
01117             args["req_perms"] = ",".join(extended_permissions)
01118         self.redirect("http://www.facebook.com/login.php?" +
01119                       urllib_parse.urlencode(args))
01120         callback()
01121 
01122     def authorize_redirect(self, extended_permissions, callback_uri=None,
01123                            cancel_uri=None, callback=None):
01124         """Redirects to an authorization request for the given FB resource.
01125 
01126         The available resource names are listed at
01127         http://wiki.developers.facebook.com/index.php/Extended_permission.
01128         The most common resource types include:
01129 
01130         * publish_stream
01131         * read_stream
01132         * email
01133         * sms
01134 
01135         extended_permissions can be a single permission name or a list of
01136         names. To get the session secret and session key, call
01137         get_authenticated_user() just as you would with
01138         authenticate_redirect().
01139 
01140         .. versionchanged:: 3.1
01141            Returns a `.Future` and takes an optional callback.  These are
01142            not strictly necessary as this method is synchronous,
01143            but they are supplied for consistency with
01144            `OAuthMixin.authorize_redirect`.
01145         """
01146         return self.authenticate_redirect(callback_uri, cancel_uri,
01147                                           extended_permissions,
01148                                           callback=callback)
01149 
01150     def get_authenticated_user(self, callback):
01151         """Fetches the authenticated Facebook user.
01152 
01153         The authenticated user includes the special Facebook attributes
01154         'session_key' and 'facebook_uid' in addition to the standard
01155         user attributes like 'name'.
01156         """
01157         self.require_setting("facebook_api_key", "Facebook Connect")
01158         session = escape.json_decode(self.get_argument("session"))
01159         self.facebook_request(
01160             method="facebook.users.getInfo",
01161             callback=functools.partial(
01162                 self._on_get_user_info, callback, session),
01163             session_key=session["session_key"],
01164             uids=session["uid"],
01165             fields="uid,first_name,last_name,name,locale,pic_square,"
01166                    "profile_url,username")
01167 
01168     def facebook_request(self, method, callback, **args):
01169         """Makes a Facebook API REST request.
01170 
01171         We automatically include the Facebook API key and signature, but
01172         it is the callers responsibility to include 'session_key' and any
01173         other required arguments to the method.
01174 
01175         The available Facebook methods are documented here:
01176         http://wiki.developers.facebook.com/index.php/API
01177 
01178         Here is an example for the stream.get() method::
01179 
01180             class MainHandler(tornado.web.RequestHandler,
01181                               tornado.auth.FacebookMixin):
01182                 @tornado.web.authenticated
01183                 @tornado.web.asynchronous
01184                 def get(self):
01185                     self.facebook_request(
01186                         method="stream.get",
01187                         callback=self._on_stream,
01188                         session_key=self.current_user["session_key"])
01189 
01190                 def _on_stream(self, stream):
01191                     if stream is None:
01192                        # Not authorized to read the stream yet?
01193                        self.redirect(self.authorize_redirect("read_stream"))
01194                        return
01195                     self.render("stream.html", stream=stream)
01196 
01197         """
01198         self.require_setting("facebook_api_key", "Facebook Connect")
01199         self.require_setting("facebook_secret", "Facebook Connect")
01200         if not method.startswith("facebook."):
01201             method = "facebook." + method
01202         args["api_key"] = self.settings["facebook_api_key"]
01203         args["v"] = "1.0"
01204         args["method"] = method
01205         args["call_id"] = str(long(time.time() * 1e6))
01206         args["format"] = "json"
01207         args["sig"] = self._signature(args)
01208         url = "http://api.facebook.com/restserver.php?" + \
01209             urllib_parse.urlencode(args)
01210         http = self.get_auth_http_client()
01211         http.fetch(url, callback=functools.partial(
01212             self._parse_response, callback))
01213 
01214     def _on_get_user_info(self, callback, session, users):
01215         if users is None:
01216             callback(None)
01217             return
01218         callback({
01219             "name": users[0]["name"],
01220             "first_name": users[0]["first_name"],
01221             "last_name": users[0]["last_name"],
01222             "uid": users[0]["uid"],
01223             "locale": users[0]["locale"],
01224             "pic_square": users[0]["pic_square"],
01225             "profile_url": users[0]["profile_url"],
01226             "username": users[0].get("username"),
01227             "session_key": session["session_key"],
01228             "session_expires": session.get("expires"),
01229         })
01230 
01231     def _parse_response(self, callback, response):
01232         if response.error:
01233             gen_log.warning("HTTP error from Facebook: %s", response.error)
01234             callback(None)
01235             return
01236         try:
01237             json = escape.json_decode(response.body)
01238         except Exception:
01239             gen_log.warning("Invalid JSON from Facebook: %r", response.body)
01240             callback(None)
01241             return
01242         if isinstance(json, dict) and json.get("error_code"):
01243             gen_log.warning("Facebook error: %d: %r", json["error_code"],
01244                             json.get("error_msg"))
01245             callback(None)
01246             return
01247         callback(json)
01248 
01249     def _signature(self, args):
01250         parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
01251         body = "".join(parts) + self.settings["facebook_secret"]
01252         if isinstance(body, unicode_type):
01253             body = body.encode("utf-8")
01254         return hashlib.md5(body).hexdigest()
01255 
01256     def get_auth_http_client(self):
01257         """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
01258 
01259         May be overridden by subclasses to use an HTTP client other than
01260         the default.
01261         """
01262         return httpclient.AsyncHTTPClient()
01263 
01264 
01265 class FacebookGraphMixin(OAuth2Mixin):
01266     """Facebook authentication using the new Graph API and OAuth2."""
01267     _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
01268     _OAUTH_AUTHORIZE_URL = "https://www.facebook.com/dialog/oauth?"
01269     _OAUTH_NO_CALLBACKS = False
01270     _FACEBOOK_BASE_URL = "https://graph.facebook.com"
01271 
01272     @_auth_return_future
01273     def get_authenticated_user(self, redirect_uri, client_id, client_secret,
01274                                code, callback, extra_fields=None):
01275         """Handles the login for the Facebook user, returning a user object.
01276 
01277         Example usage::
01278 
01279             class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin):
01280               @tornado.gen.coroutine
01281               def get(self):
01282                   if self.get_argument("code", False):
01283                       user = yield self.get_authenticated_user(
01284                           redirect_uri='/auth/facebookgraph/',
01285                           client_id=self.settings["facebook_api_key"],
01286                           client_secret=self.settings["facebook_secret"],
01287                           code=self.get_argument("code"))
01288                       # Save the user with e.g. set_secure_cookie
01289                   else:
01290                       yield self.authorize_redirect(
01291                           redirect_uri='/auth/facebookgraph/',
01292                           client_id=self.settings["facebook_api_key"],
01293                           extra_params={"scope": "read_stream,offline_access"})
01294         """
01295         http = self.get_auth_http_client()
01296         args = {
01297             "redirect_uri": redirect_uri,
01298             "code": code,
01299             "client_id": client_id,
01300             "client_secret": client_secret,
01301         }
01302 
01303         fields = set(['id', 'name', 'first_name', 'last_name',
01304                       'locale', 'picture', 'link'])
01305         if extra_fields:
01306             fields.update(extra_fields)
01307 
01308         http.fetch(self._oauth_request_token_url(**args),
01309                    functools.partial(self._on_access_token, redirect_uri, client_id,
01310                                        client_secret, callback, fields))
01311 
01312     def _on_access_token(self, redirect_uri, client_id, client_secret,
01313                          future, fields, response):
01314         if response.error:
01315             future.set_exception(AuthError('Facebook auth error: %s' % str(response)))
01316             return
01317 
01318         args = escape.parse_qs_bytes(escape.native_str(response.body))
01319         session = {
01320             "access_token": args["access_token"][-1],
01321             "expires": args.get("expires")
01322         }
01323 
01324         self.facebook_request(
01325             path="/me",
01326             callback=functools.partial(
01327                 self._on_get_user_info, future, session, fields),
01328             access_token=session["access_token"],
01329             fields=",".join(fields)
01330         )
01331 
01332     def _on_get_user_info(self, future, session, fields, user):
01333         if user is None:
01334             future.set_result(None)
01335             return
01336 
01337         fieldmap = {}
01338         for field in fields:
01339             fieldmap[field] = user.get(field)
01340 
01341         fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")})
01342         future.set_result(fieldmap)
01343 
01344     @_auth_return_future
01345     def facebook_request(self, path, callback, access_token=None,
01346                          post_args=None, **args):
01347         """Fetches the given relative API path, e.g., "/btaylor/picture"
01348 
01349         If the request is a POST, ``post_args`` should be provided. Query
01350         string arguments should be given as keyword arguments.
01351 
01352         An introduction to the Facebook Graph API can be found at
01353         http://developers.facebook.com/docs/api
01354 
01355         Many methods require an OAuth access token which you can
01356         obtain through `~OAuth2Mixin.authorize_redirect` and
01357         `get_authenticated_user`. The user returned through that
01358         process includes an ``access_token`` attribute that can be
01359         used to make authenticated requests via this method.
01360 
01361         Example usage::
01362 
01363             class MainHandler(tornado.web.RequestHandler,
01364                               tornado.auth.FacebookGraphMixin):
01365                 @tornado.web.authenticated
01366                 @tornado.gen.coroutine
01367                 def get(self):
01368                     new_entry = yield self.facebook_request(
01369                         "/me/feed",
01370                         post_args={"message": "I am posting from my Tornado application!"},
01371                         access_token=self.current_user["access_token"])
01372 
01373                     if not new_entry:
01374                         # Call failed; perhaps missing permission?
01375                         yield self.authorize_redirect()
01376                         return
01377                     self.finish("Posted a message!")
01378 
01379         The given path is relative to ``self._FACEBOOK_BASE_URL``,
01380         by default "https://graph.facebook.com".
01381 
01382         .. versionchanged:: 3.1
01383            Added the ability to override ``self._FACEBOOK_BASE_URL``.
01384         """
01385         url = self._FACEBOOK_BASE_URL + path
01386         all_args = {}
01387         if access_token:
01388             all_args["access_token"] = access_token
01389             all_args.update(args)
01390 
01391         if all_args:
01392             url += "?" + urllib_parse.urlencode(all_args)
01393         callback = functools.partial(self._on_facebook_request, callback)
01394         http = self.get_auth_http_client()
01395         if post_args is not None:
01396             http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
01397                        callback=callback)
01398         else:
01399             http.fetch(url, callback=callback)
01400 
01401     def _on_facebook_request(self, future, response):
01402         if response.error:
01403             future.set_exception(AuthError("Error response %s fetching %s" %
01404                                            (response.error, response.request.url)))
01405             return
01406 
01407         future.set_result(escape.json_decode(response.body))
01408 
01409     def get_auth_http_client(self):
01410         """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
01411 
01412         May be overridden by subclasses to use an HTTP client other than
01413         the default.
01414         """
01415         return httpclient.AsyncHTTPClient()
01416 
01417 
01418 def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
01419     """Calculates the HMAC-SHA1 OAuth signature for the given request.
01420 
01421     See http://oauth.net/core/1.0/#signing_process
01422     """
01423     parts = urlparse.urlparse(url)
01424     scheme, netloc, path = parts[:3]
01425     normalized_url = scheme.lower() + "://" + netloc.lower() + path
01426 
01427     base_elems = []
01428     base_elems.append(method.upper())
01429     base_elems.append(normalized_url)
01430     base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
01431                                for k, v in sorted(parameters.items())))
01432     base_string = "&".join(_oauth_escape(e) for e in base_elems)
01433 
01434     key_elems = [escape.utf8(consumer_token["secret"])]
01435     key_elems.append(escape.utf8(token["secret"] if token else ""))
01436     key = b"&".join(key_elems)
01437 
01438     hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
01439     return binascii.b2a_base64(hash.digest())[:-1]
01440 
01441 
01442 def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
01443     """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
01444 
01445     See http://oauth.net/core/1.0a/#signing_process
01446     """
01447     parts = urlparse.urlparse(url)
01448     scheme, netloc, path = parts[:3]
01449     normalized_url = scheme.lower() + "://" + netloc.lower() + path
01450 
01451     base_elems = []
01452     base_elems.append(method.upper())
01453     base_elems.append(normalized_url)
01454     base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
01455                                for k, v in sorted(parameters.items())))
01456 
01457     base_string = "&".join(_oauth_escape(e) for e in base_elems)
01458     key_elems = [escape.utf8(urllib_parse.quote(consumer_token["secret"], safe='~'))]
01459     key_elems.append(escape.utf8(urllib_parse.quote(token["secret"], safe='~') if token else ""))
01460     key = b"&".join(key_elems)
01461 
01462     hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
01463     return binascii.b2a_base64(hash.digest())[:-1]
01464 
01465 
01466 def _oauth_escape(val):
01467     if isinstance(val, unicode_type):
01468         val = val.encode("utf-8")
01469     return urllib_parse.quote(val, safe="~")
01470 
01471 
01472 def _oauth_parse_response(body):
01473     # I can't find an officially-defined encoding for oauth responses and
01474     # have never seen anyone use non-ascii.  Leave the response in a byte
01475     # string for python 2, and use utf8 on python 3.
01476     body = escape.native_str(body)
01477     p = urlparse.parse_qs(body, keep_blank_values=False)
01478     token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0])
01479 
01480     # Add the extra parameters the Provider included to the token
01481     special = ("oauth_token", "oauth_token_secret")
01482     token.update((k, p[k][0]) for k in p if k not in special)
01483     return token


rosbridge_tools
Author(s): Jonathan Mace
autogenerated on Sat Dec 27 2014 11:25:59