00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
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
00083 except ImportError:
00084 import urllib.parse as urlparse
00085
00086 try:
00087 import urllib.parse as urllib_parse
00088 except ImportError:
00089 import urllib as urllib_parse
00090
00091 try:
00092 long
00093 except NameError:
00094 long = int
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
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
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
00511
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
00712
00713 url = path
00714 else:
00715 url = self._TWITTER_BASE_URL + path + ".json"
00716
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
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
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
01474
01475
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
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