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 """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         # Verify the OpenID response via direct request to the OP
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         # Make sure we got back at least an email from attribute exchange
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             # Raw urls are useful for e.g. search which doesn't follow the
00517             # usual pattern: http://search.twitter.com/search.json
00518             url = path
00519         else:
00520             url = "http://api.twitter.com/1" + path + ".json"
00521         # Add the OAuth resource request signature if we have credentials
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         # Add the OAuth resource request signature if we have credentials
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         # Look to see if we are doing combined OpenID/OAuth
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     # Add the extra parameters the Provider included to the token
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


rosbridge_server
Author(s): Jonathan Mace
autogenerated on Mon Oct 6 2014 06:58:14