]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Remove the deprecated/obsolete auth mixins.
authorBen Darnell <ben@bendarnell.com>
Sun, 29 Mar 2015 16:05:30 +0000 (12:05 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 29 Mar 2015 16:05:30 +0000 (12:05 -0400)
Both Google's OpenID service and FriendFeed are slated to shut down
before the next release of Tornado, and FacebookMixin has been
deprecated in favor of OAuth 2.0 for a long time.

docs/auth.rst
docs/guide/security.rst
docs/releases/v4.0.0.rst
tornado/auth.py
tornado/test/auth_test.py

index b2dad547f52ddf08e50e79cb722a36aad805b975..0da249cfb1cc7de39e0149b2b342e388c6c53a71 100644 (file)
@@ -33,9 +33,6 @@
    Google
    ------
 
-   .. autoclass:: GoogleMixin
-      :members:
-   
    .. autoclass:: GoogleOAuth2Mixin
       :members:
 
    .. autoclass:: FacebookGraphMixin
       :members:
 
-   .. autoclass:: FacebookMixin
-      :members:
-
    Twitter
    -------
 
    .. autoclass:: TwitterMixin
       :members:
-
-   FriendFeed
-   ----------
-
-   .. autoclass:: FriendFeedMixin
-      :members:
-
index 793fb0500638a4122d0e2566612e0b18e75a393f..5372ff06938cf10f5d62ee48da195979afc7fee2 100644 (file)
@@ -177,19 +177,22 @@ the Google credentials in a cookie for later access:
 
 .. testcode::
 
-    class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
-        @tornado.web.asynchronous
+    class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
+                                   tornado.auth.GoogleOAuth2Mixin):
+        @tornado.gen.coroutine
         def get(self):
-            if self.get_argument("openid.mode", None):
-                self.get_authenticated_user(self._on_auth)
-                return
-            self.authenticate_redirect()
-
-        def _on_auth(self, user):
-            if not user:
-                self.authenticate_redirect()
-                return
-            # Save the user with, e.g., set_secure_cookie()
+            if self.get_argument('code', False):
+                user = yield self.get_authenticated_user(
+                    redirect_uri='http://your.site.com/auth/google',
+                    code=self.get_argument('code'))
+                # Save the user with e.g. set_secure_cookie
+            else:
+                yield self.authorize_redirect(
+                    redirect_uri='http://your.site.com/auth/google',
+                    client_id=self.settings['google_oauth']['key'],
+                    scope=['profile', 'email'],
+                    response_type='code',
+                    extra_params={'approval_prompt': 'auto'})
 
 .. testoutput::
    :hide:
index c591d08c190396b0b725a3555ea6036696ebc867..dd60ea8c3acf47f2816fb667c5c9b3d0ac6ffe5c 100644 (file)
@@ -66,7 +66,7 @@ Other notes
 `tornado.auth`
 ~~~~~~~~~~~~~~
 
-* Fixed a bug in `.FacebookMixin` on Python 3.
+* Fixed a bug in ``.FacebookMixin`` on Python 3.
 * When using the `.Future` interface, exceptions are more reliably delivered
   to the caller.
 
index 586c3dae76335affa224b1f9126b63cc5f5e74b7..800b10afe455088e49326f0aaf8ab432c8842ed0 100644 (file)
@@ -75,7 +75,7 @@ import hmac
 import time
 import uuid
 
-from tornado.concurrent import TracebackFuture, chain_future, return_future
+from tornado.concurrent import TracebackFuture, return_future
 from tornado import gen
 from tornado import httpclient
 from tornado import escape
@@ -145,9 +145,6 @@ def _auth_return_future(f):
 class OpenIdMixin(object):
     """Abstract implementation of OpenID and Attribute Exchange.
 
-    See `GoogleMixin` below for a customized example (which also
-    includes OAuth support).
-
     Class attributes:
 
     * ``_OPENID_ENDPOINT``: the identity provider's URI.
@@ -319,8 +316,7 @@ class OpenIdMixin(object):
 class OAuthMixin(object):
     """Abstract implementation of OAuth 1.0 and 1.0a.
 
-    See `TwitterMixin` and `FriendFeedMixin` below for example implementations,
-    or `GoogleMixin` for an OAuth/OpenID hybrid.
+    See `TwitterMixin` below for an example implementation.
 
     Class attributes:
 
@@ -572,7 +568,8 @@ class OAuthMixin(object):
 class OAuth2Mixin(object):
     """Abstract implementation of OAuth 2.0.
 
-    See `FacebookGraphMixin` below for an example implementation.
+    See `FacebookGraphMixin` or `GoogleOAuth2Mixin` below for example
+    implementations.
 
     Class attributes:
 
@@ -774,240 +771,6 @@ class TwitterMixin(OAuthMixin):
         raise gen.Return(user)
 
 
-class FriendFeedMixin(OAuthMixin):
-    """FriendFeed OAuth authentication.
-
-    To authenticate with FriendFeed, register your application with
-    FriendFeed at http://friendfeed.com/api/applications. Then copy
-    your Consumer Key and Consumer Secret to the application
-    `~tornado.web.Application.settings` ``friendfeed_consumer_key``
-    and ``friendfeed_consumer_secret``. Use this mixin on the handler
-    for the URL you registered as your application's Callback URL.
-
-    When your application is set up, you can use this mixin like this
-    to authenticate the user with FriendFeed and get access to their feed:
-
-    .. testcode::
-
-        class FriendFeedLoginHandler(tornado.web.RequestHandler,
-                                     tornado.auth.FriendFeedMixin):
-            @tornado.gen.coroutine
-            def get(self):
-                if self.get_argument("oauth_token", None):
-                    user = yield self.get_authenticated_user()
-                    # Save the user using e.g. set_secure_cookie()
-                else:
-                    yield self.authorize_redirect()
-
-    .. testoutput::
-       :hide:
-
-
-    The user object returned by `~OAuthMixin.get_authenticated_user()` includes the
-    attributes ``username``, ``name``, and ``description`` in addition to
-    ``access_token``. You should save the access token with the user;
-    it is required to make requests on behalf of the user later with
-    `friendfeed_request()`.
-    """
-    _OAUTH_VERSION = "1.0"
-    _OAUTH_REQUEST_TOKEN_URL = "https://friendfeed.com/account/oauth/request_token"
-    _OAUTH_ACCESS_TOKEN_URL = "https://friendfeed.com/account/oauth/access_token"
-    _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
-    _OAUTH_NO_CALLBACKS = True
-    _OAUTH_VERSION = "1.0"
-
-    @_auth_return_future
-    def friendfeed_request(self, path, callback, access_token=None,
-                           post_args=None, **args):
-        """Fetches the given relative API path, e.g., "/bret/friends"
-
-        If the request is a POST, ``post_args`` should be provided. Query
-        string arguments should be given as keyword arguments.
-
-        All the FriendFeed methods are documented at
-        http://friendfeed.com/api/documentation.
-
-        Many methods require an OAuth access token which you can
-        obtain through `~OAuthMixin.authorize_redirect` and
-        `~OAuthMixin.get_authenticated_user`. The user returned
-        through that process includes an ``access_token`` attribute that
-        can be used to make authenticated requests via this
-        method.
-
-        Example usage:
-
-        .. testcode::
-
-            class MainHandler(tornado.web.RequestHandler,
-                              tornado.auth.FriendFeedMixin):
-                @tornado.web.authenticated
-                @tornado.gen.coroutine
-                def get(self):
-                    new_entry = yield self.friendfeed_request(
-                        "/entry",
-                        post_args={"body": "Testing Tornado Web Server"},
-                        access_token=self.current_user["access_token"])
-
-                    if not new_entry:
-                        # Call failed; perhaps missing permission?
-                        yield self.authorize_redirect()
-                        return
-                    self.finish("Posted a message!")
-
-        .. testoutput::
-           :hide:
-
-        """
-        # Add the OAuth resource request signature if we have credentials
-        url = "http://friendfeed-api.com/v2" + path
-        if access_token:
-            all_args = {}
-            all_args.update(args)
-            all_args.update(post_args or {})
-            method = "POST" if post_args is not None else "GET"
-            oauth = self._oauth_request_parameters(
-                url, access_token, all_args, method=method)
-            args.update(oauth)
-        if args:
-            url += "?" + urllib_parse.urlencode(args)
-        callback = functools.partial(self._on_friendfeed_request, callback)
-        http = self.get_auth_http_client()
-        if post_args is not None:
-            http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
-                       callback=callback)
-        else:
-            http.fetch(url, callback=callback)
-
-    def _on_friendfeed_request(self, future, response):
-        if response.error:
-            future.set_exception(AuthError(
-                "Error response %s fetching %s" % (response.error,
-                                                   response.request.url)))
-            return
-        future.set_result(escape.json_decode(response.body))
-
-    def _oauth_consumer_token(self):
-        self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
-        self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
-        return dict(
-            key=self.settings["friendfeed_consumer_key"],
-            secret=self.settings["friendfeed_consumer_secret"])
-
-    @gen.coroutine
-    def _oauth_get_user_future(self, access_token, callback):
-        user = yield self.friendfeed_request(
-            "/feedinfo/" + access_token["username"],
-            include="id,name,description", access_token=access_token)
-        if user:
-            user["username"] = user["id"]
-        callback(user)
-
-    def _parse_user_response(self, callback, user):
-        if user:
-            user["username"] = user["id"]
-        callback(user)
-
-
-class GoogleMixin(OpenIdMixin, OAuthMixin):
-    """Google Open ID / OAuth authentication.
-
-    .. deprecated:: 4.0
-       New applications should use `GoogleOAuth2Mixin`
-       below instead of this class. As of May 19, 2014, Google has stopped
-       supporting registration-free authentication.
-
-    No application registration is necessary to use Google for
-    authentication or to access Google resources on behalf of a user.
-
-    Google implements both OpenID and OAuth in a hybrid mode.  If you
-    just need the user's identity, use
-    `~OpenIdMixin.authenticate_redirect`.  If you need to make
-    requests to Google on behalf of the user, use
-    `authorize_redirect`.  On return, parse the response with
-    `~OpenIdMixin.get_authenticated_user`. We send a dict containing
-    the values for the user, including ``email``, ``name``, and
-    ``locale``.
-
-    Example usage:
-
-    .. testcode::
-
-        class GoogleLoginHandler(tornado.web.RequestHandler,
-                                 tornado.auth.GoogleMixin):
-           @tornado.gen.coroutine
-           def get(self):
-               if self.get_argument("openid.mode", None):
-                   user = yield self.get_authenticated_user()
-                   # Save the user with e.g. set_secure_cookie()
-               else:
-                   yield self.authenticate_redirect()
-
-    .. testoutput::
-       :hide:
-
-    """
-    _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
-    _OAUTH_ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken"
-
-    @return_future
-    def authorize_redirect(self, oauth_scope, callback_uri=None,
-                           ax_attrs=["name", "email", "language", "username"],
-                           callback=None):
-        """Authenticates and authorizes for the given Google resource.
-
-        Some of the available resources which can be used in the ``oauth_scope``
-        argument are:
-
-        * Gmail Contacts - http://www.google.com/m8/feeds/
-        * Calendar - http://www.google.com/calendar/feeds/
-        * Finance - http://finance.google.com/finance/feeds/
-
-        You can authorize multiple resources by separating the resource
-        URLs with a space.
-
-        .. versionchanged:: 3.1
-           Returns a `.Future` and takes an optional callback.  These are
-           not strictly necessary as this method is synchronous,
-           but they are supplied for consistency with
-           `OAuthMixin.authorize_redirect`.
-        """
-        callback_uri = callback_uri or self.request.uri
-        args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
-                                 oauth_scope=oauth_scope)
-        self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
-        callback()
-
-    @_auth_return_future
-    def get_authenticated_user(self, callback):
-        """Fetches the authenticated user data upon redirect."""
-        # Look to see if we are doing combined OpenID/OAuth
-        oauth_ns = ""
-        for name, values in self.request.arguments.items():
-            if name.startswith("openid.ns.") and \
-                    values[-1] == b"http://specs.openid.net/extensions/oauth/1.0":
-                oauth_ns = name[10:]
-                break
-        token = self.get_argument("openid." + oauth_ns + ".request_token", "")
-        if token:
-            http = self.get_auth_http_client()
-            token = dict(key=token, secret="")
-            http.fetch(self._oauth_access_token_url(token),
-                       functools.partial(self._on_access_token, callback))
-        else:
-            chain_future(OpenIdMixin.get_authenticated_user(self),
-                         callback)
-
-    def _oauth_consumer_token(self):
-        self.require_setting("google_consumer_key", "Google OAuth")
-        self.require_setting("google_consumer_secret", "Google OAuth")
-        return dict(
-            key=self.settings["google_consumer_key"],
-            secret=self.settings["google_consumer_secret"])
-
-    def _oauth_get_user_future(self, access_token):
-        return OpenIdMixin.get_authenticated_user(self)
-
-
 class GoogleOAuth2Mixin(OAuth2Mixin):
     """Google authentication using OAuth2.
 
@@ -1091,227 +854,6 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
         return httpclient.AsyncHTTPClient()
 
 
-class FacebookMixin(object):
-    """Facebook Connect authentication.
-
-    .. deprecated:: 1.1
-       New applications should use `FacebookGraphMixin`
-       below instead of this class.  This class does not support the
-       Future-based interface seen on other classes in this module.
-
-    To authenticate with Facebook, register your application with
-    Facebook at http://www.facebook.com/developers/apps.php. Then
-    copy your API Key and Application Secret to the application settings
-    ``facebook_api_key`` and ``facebook_secret``.
-
-    When your application is set up, you can use this mixin like this
-    to authenticate the user with Facebook:
-
-    .. testcode::
-
-        class FacebookHandler(tornado.web.RequestHandler,
-                              tornado.auth.FacebookMixin):
-            @tornado.web.asynchronous
-            def get(self):
-                if self.get_argument("session", None):
-                    self.get_authenticated_user(self._on_auth)
-                    return
-                yield self.authenticate_redirect()
-
-            def _on_auth(self, user):
-                if not user:
-                    raise tornado.web.HTTPError(500, "Facebook auth failed")
-                # Save the user using, e.g., set_secure_cookie()
-
-    .. testoutput::
-       :hide:
-
-    The user object returned by `get_authenticated_user` includes the
-    attributes ``facebook_uid`` and ``name`` in addition to session attributes
-    like ``session_key``. You should save the session key with the user; it is
-    required to make requests on behalf of the user later with
-    `facebook_request`.
-    """
-    @return_future
-    def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
-                              extended_permissions=None, callback=None):
-        """Authenticates/installs this app for the current user.
-
-        .. versionchanged:: 3.1
-           Returns a `.Future` and takes an optional callback.  These are
-           not strictly necessary as this method is synchronous,
-           but they are supplied for consistency with
-           `OAuthMixin.authorize_redirect`.
-        """
-        self.require_setting("facebook_api_key", "Facebook Connect")
-        callback_uri = callback_uri or self.request.uri
-        args = {
-            "api_key": self.settings["facebook_api_key"],
-            "v": "1.0",
-            "fbconnect": "true",
-            "display": "page",
-            "next": urlparse.urljoin(self.request.full_url(), callback_uri),
-            "return_session": "true",
-        }
-        if cancel_uri:
-            args["cancel_url"] = urlparse.urljoin(
-                self.request.full_url(), cancel_uri)
-        if extended_permissions:
-            if isinstance(extended_permissions, (unicode_type, bytes)):
-                extended_permissions = [extended_permissions]
-            args["req_perms"] = ",".join(extended_permissions)
-        self.redirect("http://www.facebook.com/login.php?" +
-                      urllib_parse.urlencode(args))
-        callback()
-
-    def authorize_redirect(self, extended_permissions, callback_uri=None,
-                           cancel_uri=None, callback=None):
-        """Redirects to an authorization request for the given FB resource.
-
-        The available resource names are listed at
-        http://wiki.developers.facebook.com/index.php/Extended_permission.
-        The most common resource types include:
-
-        * publish_stream
-        * read_stream
-        * email
-        * sms
-
-        extended_permissions can be a single permission name or a list of
-        names. To get the session secret and session key, call
-        get_authenticated_user() just as you would with
-        authenticate_redirect().
-
-        .. versionchanged:: 3.1
-           Returns a `.Future` and takes an optional callback.  These are
-           not strictly necessary as this method is synchronous,
-           but they are supplied for consistency with
-           `OAuthMixin.authorize_redirect`.
-        """
-        return self.authenticate_redirect(callback_uri, cancel_uri,
-                                          extended_permissions,
-                                          callback=callback)
-
-    def get_authenticated_user(self, callback):
-        """Fetches the authenticated Facebook user.
-
-        The authenticated user includes the special Facebook attributes
-        'session_key' and 'facebook_uid' in addition to the standard
-        user attributes like 'name'.
-        """
-        self.require_setting("facebook_api_key", "Facebook Connect")
-        session = escape.json_decode(self.get_argument("session"))
-        self.facebook_request(
-            method="facebook.users.getInfo",
-            callback=functools.partial(
-                self._on_get_user_info, callback, session),
-            session_key=session["session_key"],
-            uids=session["uid"],
-            fields="uid,first_name,last_name,name,locale,pic_square,"
-                   "profile_url,username")
-
-    def facebook_request(self, method, callback, **args):
-        """Makes a Facebook API REST request.
-
-        We automatically include the Facebook API key and signature, but
-        it is the callers responsibility to include 'session_key' and any
-        other required arguments to the method.
-
-        The available Facebook methods are documented here:
-        http://wiki.developers.facebook.com/index.php/API
-
-        Here is an example for the stream.get() method:
-
-        .. testcode::
-
-            class MainHandler(tornado.web.RequestHandler,
-                              tornado.auth.FacebookMixin):
-                @tornado.web.authenticated
-                @tornado.web.asynchronous
-                def get(self):
-                    self.facebook_request(
-                        method="stream.get",
-                        callback=self._on_stream,
-                        session_key=self.current_user["session_key"])
-
-                def _on_stream(self, stream):
-                    if stream is None:
-                       # Not authorized to read the stream yet?
-                       self.redirect(self.authorize_redirect("read_stream"))
-                       return
-                    self.render("stream.html", stream=stream)
-
-        .. testoutput::
-           :hide:
-
-        """
-        self.require_setting("facebook_api_key", "Facebook Connect")
-        self.require_setting("facebook_secret", "Facebook Connect")
-        if not method.startswith("facebook."):
-            method = "facebook." + method
-        args["api_key"] = self.settings["facebook_api_key"]
-        args["v"] = "1.0"
-        args["method"] = method
-        args["call_id"] = str(long(time.time() * 1e6))
-        args["format"] = "json"
-        args["sig"] = self._signature(args)
-        url = "http://api.facebook.com/restserver.php?" + \
-            urllib_parse.urlencode(args)
-        http = self.get_auth_http_client()
-        http.fetch(url, callback=functools.partial(
-            self._parse_response, callback))
-
-    def _on_get_user_info(self, callback, session, users):
-        if users is None:
-            callback(None)
-            return
-        callback({
-            "name": users[0]["name"],
-            "first_name": users[0]["first_name"],
-            "last_name": users[0]["last_name"],
-            "uid": users[0]["uid"],
-            "locale": users[0]["locale"],
-            "pic_square": users[0]["pic_square"],
-            "profile_url": users[0]["profile_url"],
-            "username": users[0].get("username"),
-            "session_key": session["session_key"],
-            "session_expires": session.get("expires"),
-        })
-
-    def _parse_response(self, callback, response):
-        if response.error:
-            gen_log.warning("HTTP error from Facebook: %s", response.error)
-            callback(None)
-            return
-        try:
-            json = escape.json_decode(response.body)
-        except Exception:
-            gen_log.warning("Invalid JSON from Facebook: %r", response.body)
-            callback(None)
-            return
-        if isinstance(json, dict) and json.get("error_code"):
-            gen_log.warning("Facebook error: %d: %r", json["error_code"],
-                            json.get("error_msg"))
-            callback(None)
-            return
-        callback(json)
-
-    def _signature(self, args):
-        parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
-        body = "".join(parts) + self.settings["facebook_secret"]
-        if isinstance(body, unicode_type):
-            body = body.encode("utf-8")
-        return hashlib.md5(body).hexdigest()
-
-    def get_auth_http_client(self):
-        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
-
-        May be overridden by subclasses to use an HTTP client other than
-        the default.
-        """
-        return httpclient.AsyncHTTPClient()
-
-
 class FacebookGraphMixin(OAuth2Mixin):
     """Facebook authentication using the new Graph API and OAuth2."""
     _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
index 254e1ae13c6862c319f1d2b2f952c309111b2765..541ecf16f3888993d2a54f164a85fd5450bd71a1 100644 (file)
@@ -5,7 +5,7 @@
 
 
 from __future__ import absolute_import, division, print_function, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, GoogleMixin, AuthError
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError
 from tornado.concurrent import Future
 from tornado.escape import json_decode
 from tornado import gen
@@ -238,28 +238,6 @@ class TwitterServerVerifyCredentialsHandler(RequestHandler):
         self.write(dict(screen_name='foo', name='Foo'))
 
 
-class GoogleOpenIdClientLoginHandler(RequestHandler, GoogleMixin):
-    def initialize(self, test):
-        self._OPENID_ENDPOINT = test.get_url('/openid/server/authenticate')
-
-    @asynchronous
-    def get(self):
-        if self.get_argument("openid.mode", None):
-            self.get_authenticated_user(self.on_user)
-            return
-        res = self.authenticate_redirect()
-        assert isinstance(res, Future)
-        assert res.done()
-
-    def on_user(self, user):
-        if user is None:
-            raise Exception("user is None")
-        self.finish(user)
-
-    def get_auth_http_client(self):
-        return self.settings['http_client']
-
-
 class AuthTest(AsyncHTTPTestCase):
     def get_app(self):
         return Application(
@@ -286,7 +264,6 @@ class AuthTest(AsyncHTTPTestCase):
                 ('/twitter/client/login_gen_coroutine', TwitterClientLoginGenCoroutineHandler, dict(test=self)),
                 ('/twitter/client/show_user', TwitterClientShowUserHandler, dict(test=self)),
                 ('/twitter/client/show_user_future', TwitterClientShowUserFutureHandler, dict(test=self)),
-                ('/google/client/openid_login', GoogleOpenIdClientLoginHandler, dict(test=self)),
 
                 # simulated servers
                 ('/openid/server/authenticate', OpenIdServerAuthenticateHandler),
@@ -436,16 +413,3 @@ class AuthTest(AsyncHTTPTestCase):
         response = self.fetch('/twitter/client/show_user_future?name=error')
         self.assertEqual(response.code, 500)
         self.assertIn(b'Error response HTTP 500', response.body)
-
-    def test_google_redirect(self):
-        # same as test_openid_redirect
-        response = self.fetch('/google/client/openid_login', follow_redirects=False)
-        self.assertEqual(response.code, 302)
-        self.assertTrue(
-            '/openid/server/authenticate?' in response.headers['Location'])
-
-    def test_google_get_user(self):
-        response = self.fetch('/google/client/openid_login?openid.mode=blah&openid.ns.ax=http://openid.net/srv/ax/1.0&openid.ax.type.email=http://axschema.org/contact/email&openid.ax.value.email=foo@example.com', follow_redirects=False)
-        response.rethrow()
-        parsed = json_decode(response.body)
-        self.assertEqual(parsed["email"], "foo@example.com")