From: Ben Darnell Date: Fri, 6 Jul 2018 22:23:22 +0000 (-0400) Subject: auth: Remove deprecated callback interfaces X-Git-Tag: v6.0.0b1~48^2~22 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=71ffeedb7a7f536b7ca1250a930d469db887ca6f;p=thirdparty%2Ftornado.git auth: Remove deprecated callback interfaces --- diff --git a/tornado/auth.py b/tornado/auth.py index ab1a8503a..07a3f5d70 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -54,93 +54,29 @@ Example usage for Google OAuth: .. testoutput:: :hide: - -.. versionchanged:: 4.0 - All of the callback interfaces in this module are now guaranteed - to run their callback with an argument of ``None`` on error. - Previously some functions would do this while others would simply - terminate the request on their own. This change also ensures that - errors are more consistently reported through the ``Future`` interfaces. """ from __future__ import absolute_import, division, print_function import base64 import binascii -import functools import hashlib import hmac import time +import urllib.parse import uuid -import warnings -from tornado.concurrent import (Future, _non_deprecated_return_future, - future_set_exc_info, chain_future, - future_set_result_unless_cancelled) from tornado import gen from tornado import httpclient from tornado import escape from tornado.httputil import url_concat -from tornado.log import gen_log -from tornado.stack_context import ExceptionStackContext -from tornado.util import unicode_type, ArgReplacer, PY3 - -if PY3: - import urllib.parse as urlparse - import urllib.parse as urllib_parse - long = int -else: - import urlparse - import urllib as urllib_parse +from tornado.util import unicode_type class AuthError(Exception): pass -def _auth_future_to_callback(callback, future): - try: - result = future.result() - except AuthError as e: - gen_log.warning(str(e)) - result = None - callback(result) - - -def _auth_return_future(f): - """Similar to tornado.concurrent.return_future, but uses the auth - module's legacy callback interface. - - Note that when using this decorator the ``callback`` parameter - inside the function will actually be a future. - - .. deprecated:: 5.1 - Will be removed in 6.0. - """ - replacer = ArgReplacer(f, 'callback') - - @functools.wraps(f) - def wrapper(*args, **kwargs): - future = Future() - callback, args, kwargs = replacer.replace(future, args, kwargs) - if callback is not None: - warnings.warn("callback arguments are deprecated, use the returned Future instead", - DeprecationWarning) - future.add_done_callback( - functools.partial(_auth_future_to_callback, callback)) - - def handle_exception(typ, value, tb): - if future.done(): - return False - else: - future_set_exc_info(future, (typ, value, tb)) - return True - with ExceptionStackContext(handle_exception, delay_warning=True): - f(*args, **kwargs) - return future - return wrapper - - class OpenIdMixin(object): """Abstract implementation of OpenID and Attribute Exchange. @@ -148,10 +84,8 @@ class OpenIdMixin(object): * ``_OPENID_ENDPOINT``: the identity provider's URI. """ - @_non_deprecated_return_future def authenticate_redirect(self, callback_uri=None, - ax_attrs=["name", "email", "language", "username"], - callback=None): + ax_attrs=["name", "email", "language", "username"]): """Redirects to the authentication URL for this service. After authentication, the service will redirect back to the given @@ -162,24 +96,17 @@ class OpenIdMixin(object): all those attributes for your app, you can request fewer with the ax_attrs keyword argument. - .. 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`. - - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument and returned awaitable will be removed - in Tornado 6.0; this will be an ordinary synchronous function. + The ``callback`` argument was removed and this method no + longer returns an awaitable object. It is now an ordinary + synchronous function. """ callback_uri = callback_uri or self.request.uri args = self._openid_args(callback_uri, ax_attrs=ax_attrs) - self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args)) - callback() + self.redirect(self._OPENID_ENDPOINT + "?" + urllib.parse.urlencode(args)) - @_auth_return_future - def get_authenticated_user(self, callback, http_client=None): + async def get_authenticated_user(self, http_client=None): """Fetches the authenticated user data upon redirect. This method should be called by the handler that receives the @@ -190,10 +117,10 @@ class OpenIdMixin(object): The result of this method will generally be used to set a cookie. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned + awaitable object instead. """ # Verify the OpenID response via direct request to the OP args = dict((k, v[-1]) for k, v in self.request.arguments.items()) @@ -201,12 +128,11 @@ class OpenIdMixin(object): url = self._OPENID_ENDPOINT if http_client is None: http_client = self.get_auth_http_client() - fut = http_client.fetch(url, method="POST", body=urllib_parse.urlencode(args)) - fut.add_done_callback(functools.partial( - self._on_authentication_verified, callback)) + resp = await http_client.fetch(url, method="POST", body=urllib.parse.urlencode(args)) + return self._on_authentication_verified(resp) def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None): - url = urlparse.urljoin(self.request.full_url(), callback_uri) + url = urllib.parse.urljoin(self.request.full_url(), callback_uri) args = { "openid.ns": "http://specs.openid.net/auth/2.0", "openid.claimed_id": @@ -214,7 +140,7 @@ class OpenIdMixin(object): "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select", "openid.return_to": url, - "openid.realm": urlparse.urljoin(url, '/'), + "openid.realm": urllib.parse.urljoin(url, '/'), "openid.mode": "checkid_setup", } if ax_attrs: @@ -253,17 +179,9 @@ class OpenIdMixin(object): }) return args - def _on_authentication_verified(self, future, response_fut): - try: - response = response_fut.result() - except Exception as e: - future.set_exception(AuthError( - "Error response %s" % e)) - return + def _on_authentication_verified(self, response): if b"is_valid:true" not in response.body: - future.set_exception(AuthError( - "Invalid OpenID response: %s" % response.body)) - return + raise AuthError("Invalid OpenID response: %s" % response.body) # Make sure we got back at least an email from attribute exchange ax_ns = None @@ -316,7 +234,7 @@ class OpenIdMixin(object): claimed_id = self.get_argument("openid.claimed_id", None) if claimed_id: user["claimed_id"] = claimed_id - future_set_result_unless_cancelled(future, user) + return user def get_auth_http_client(self): """Returns the `.AsyncHTTPClient` instance to be used for auth requests. @@ -343,9 +261,8 @@ class OAuthMixin(object): Subclasses must also override the `_oauth_get_user_future` and `_oauth_consumer_token` methods. """ - @_non_deprecated_return_future - def authorize_redirect(self, callback_uri=None, extra_params=None, - http_client=None, callback=None): + async def authorize_redirect(self, callback_uri=None, extra_params=None, + http_client=None): """Redirects the user to obtain OAuth authorization for this service. The ``callback_uri`` may be omitted if you have previously @@ -367,10 +284,10 @@ class OAuthMixin(object): Now returns a `.Future` and takes an optional callback, for compatibility with `.gen.coroutine`. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned + awaitable object instead. """ if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False): @@ -378,24 +295,14 @@ class OAuthMixin(object): if http_client is None: http_client = self.get_auth_http_client() if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": - fut = http_client.fetch( + response = await http_client.fetch( self._oauth_request_token_url(callback_uri=callback_uri, extra_params=extra_params)) - fut.add_done_callback(functools.partial( - self._on_request_token, - self._OAUTH_AUTHORIZE_URL, - callback_uri, - callback)) else: - fut = http_client.fetch(self._oauth_request_token_url()) - fut.add_done_callback( - functools.partial( - self._on_request_token, self._OAUTH_AUTHORIZE_URL, - callback_uri, - callback)) - - @_auth_return_future - def get_authenticated_user(self, callback, http_client=None): + response = await http_client.fetch(self._oauth_request_token_url()) + self._on_request_token(self._OAUTH_AUTHORIZE_URL, callback_uri, response) + + async def get_authenticated_user(self, http_client=None): """Gets the OAuth authorized user and access token. This method should be called from the handler for your @@ -406,33 +313,33 @@ class OAuthMixin(object): also contain other fields such as ``name``, depending on the service used. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned + awaitable object instead. """ - future = callback request_key = escape.utf8(self.get_argument("oauth_token")) oauth_verifier = self.get_argument("oauth_verifier", None) request_cookie = self.get_cookie("_oauth_request_token") if not request_cookie: - future.set_exception(AuthError( - "Missing OAuth request token cookie")) - return + raise AuthError("Missing OAuth request token cookie") self.clear_cookie("_oauth_request_token") cookie_key, cookie_secret = [ base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")] if cookie_key != request_key: - future.set_exception(AuthError( - "Request token does not match cookie")) - return + raise AuthError("Request token does not match cookie") token = dict(key=cookie_key, secret=cookie_secret) if oauth_verifier: token["verifier"] = oauth_verifier if http_client is None: http_client = self.get_auth_http_client() - fut = http_client.fetch(self._oauth_access_token_url(token)) - fut.add_done_callback(functools.partial(self._on_access_token, callback)) + response = await http_client.fetch(self._oauth_access_token_url(token)) + access_token = _oauth_parse_response(response.body) + user = await self._oauth_get_user_future(access_token) + if not user: + raise AuthError("Error getting user") + user["access_token"] = access_token + return user def _oauth_request_token_url(self, callback_uri=None, extra_params=None): consumer_token = self._oauth_consumer_token() @@ -448,7 +355,7 @@ class OAuthMixin(object): if callback_uri == "oob": args["oauth_callback"] = "oob" elif callback_uri: - args["oauth_callback"] = urlparse.urljoin( + args["oauth_callback"] = urllib.parse.urljoin( self.request.full_url(), callback_uri) if extra_params: args.update(extra_params) @@ -457,28 +364,21 @@ class OAuthMixin(object): signature = _oauth_signature(consumer_token, "GET", url, args) args["oauth_signature"] = signature - return url + "?" + urllib_parse.urlencode(args) - - def _on_request_token(self, authorize_url, callback_uri, callback, - response_fut): - try: - response = response_fut.result() - except Exception as e: - raise Exception("Could not get request token: %s" % e) + return url + "?" + urllib.parse.urlencode(args) + + def _on_request_token(self, authorize_url, callback_uri, response): request_token = _oauth_parse_response(response.body) data = (base64.b64encode(escape.utf8(request_token["key"])) + b"|" + base64.b64encode(escape.utf8(request_token["secret"]))) self.set_cookie("_oauth_request_token", data) args = dict(oauth_token=request_token["key"]) if callback_uri == "oob": - self.finish(authorize_url + "?" + urllib_parse.urlencode(args)) - callback() + self.finish(authorize_url + "?" + urllib.parse.urlencode(args)) return elif callback_uri: - args["oauth_callback"] = urlparse.urljoin( + args["oauth_callback"] = urllib.parse.urljoin( self.request.full_url(), callback_uri) - self.redirect(authorize_url + "?" + urllib_parse.urlencode(args)) - callback() + self.redirect(authorize_url + "?" + urllib.parse.urlencode(args)) def _oauth_access_token_url(self, request_token): consumer_token = self._oauth_consumer_token() @@ -502,20 +402,8 @@ class OAuthMixin(object): request_token) args["oauth_signature"] = signature - return url + "?" + urllib_parse.urlencode(args) + return url + "?" + urllib.parse.urlencode(args) - def _on_access_token(self, future, response_fut): - try: - response = response_fut.result() - except Exception: - future.set_exception(AuthError("Could not fetch access token")) - return - - access_token = _oauth_parse_response(response.body) - fut = self._oauth_get_user_future(access_token) - fut = gen.convert_yielded(fut) - fut.add_done_callback( - functools.partial(self._on_oauth_get_user, access_token, future)) def _oauth_consumer_token(self): """Subclasses must override this to return their OAuth consumer keys. @@ -524,12 +412,11 @@ class OAuthMixin(object): """ raise NotImplementedError() - @_non_deprecated_return_future - def _oauth_get_user_future(self, access_token, callback): + async def _oauth_get_user_future(self, access_token): """Subclasses must override this to get basic information about the user. - Should return a `.Future` whose result is a dictionary + Should be a coroutine whose result is a dictionary containing information about the user, which may have been retrieved by using ``access_token`` to make a request to the service. @@ -537,38 +424,16 @@ class OAuthMixin(object): The access token will be added to the returned dictionary to make the result of `get_authenticated_user`. - For backwards compatibility, the callback-based ``_oauth_get_user`` - method is also supported. - .. versionchanged:: 5.1 Subclasses may also define this method with ``async def``. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``_oauth_get_user`` fallback is deprecated and support for it - will be removed in 6.0. + A synchronous fallback to ``_oauth_get_user`` was removed. """ - warnings.warn("_oauth_get_user is deprecated, override _oauth_get_user_future instead", - DeprecationWarning) - # By default, call the old-style _oauth_get_user, but new code - # should override this method instead. - self._oauth_get_user(access_token, callback) - - def _oauth_get_user(self, access_token, callback): raise NotImplementedError() - def _on_oauth_get_user(self, access_token, future, user_future): - if user_future.exception() is not None: - future.set_exception(user_future.exception()) - return - user = user_future.result() - if not user: - future.set_exception(AuthError("Error getting user")) - return - user["access_token"] = access_token - future_set_result_unless_cancelled(future, user) - def _oauth_request_parameters(self, url, access_token, parameters={}, method="GET"): """Returns the OAuth parameters as a dict for the given request. @@ -617,10 +482,9 @@ class OAuth2Mixin(object): * ``_OAUTH_AUTHORIZE_URL``: The service's authorization url. * ``_OAUTH_ACCESS_TOKEN_URL``: The service's access token url. """ - @_non_deprecated_return_future def authorize_redirect(self, redirect_uri=None, client_id=None, client_secret=None, extra_params=None, - callback=None, scope=None, response_type="code"): + scope=None, response_type="code"): """Redirects the user to obtain OAuth authorization for this service. Some providers require that you register a redirect URL with @@ -629,16 +493,10 @@ class OAuth2Mixin(object): ``get_authenticated_user`` in the handler for your redirect URL to complete the authorization process. - .. 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`. - - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument and returned awaitable will be removed - in Tornado 6.0; this will be an ordinary synchronous function. + The ``callback`` argument and returned awaitable were removed; + this is now an ordinary synchronous function. """ args = { "redirect_uri": redirect_uri, @@ -651,7 +509,6 @@ class OAuth2Mixin(object): args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) - callback() def _oauth_request_token_url(self, redirect_uri=None, client_id=None, client_secret=None, code=None, @@ -667,9 +524,8 @@ class OAuth2Mixin(object): args.update(extra_params) return url_concat(url, args) - @_auth_return_future - def oauth2_request(self, url, callback, access_token=None, - post_args=None, **args): + async def oauth2_request(self, url, access_token=None, + post_args=None, **args): """Fetches the given URL auth an OAuth2 access token. If the request is a POST, ``post_args`` should be provided. Query @@ -699,10 +555,9 @@ class OAuth2Mixin(object): .. versionadded:: 4.3 - .. deprecated:: 5.1 + .. versionchanged::: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned awaitable object instead. """ all_args = {} if access_token: @@ -710,23 +565,13 @@ class OAuth2Mixin(object): all_args.update(args) if all_args: - url += "?" + urllib_parse.urlencode(all_args) - callback = functools.partial(self._on_oauth2_request, callback) + url += "?" + urllib.parse.urlencode(all_args) http = self.get_auth_http_client() if post_args is not None: - fut = http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args)) + response = await http.fetch(url, method="POST", body=urllib.parse.urlencode(post_args)) else: - fut = http.fetch(url) - fut.add_done_callback(callback) - - def _on_oauth2_request(self, future, response_fut): - try: - response = response_fut.result() - except Exception as e: - future.set_exception(AuthError("Error response %s" % e)) - return - - future_set_result_unless_cancelled(future, escape.json_decode(response.body)) + response = await http.fetch(url) + return escape.json_decode(response.body) def get_auth_http_client(self): """Returns the `.AsyncHTTPClient` instance to be used for auth requests. @@ -778,8 +623,7 @@ class TwitterMixin(OAuthMixin): _OAUTH_NO_CALLBACKS = False _TWITTER_BASE_URL = "https://api.twitter.com/1.1" - @_non_deprecated_return_future - def authenticate_redirect(self, callback_uri=None, callback=None): + async def authenticate_redirect(self, callback_uri=None): """Just like `~OAuthMixin.authorize_redirect`, but auto-redirects if authorized. @@ -790,20 +634,16 @@ class TwitterMixin(OAuthMixin): Now returns a `.Future` and takes an optional callback, for compatibility with `.gen.coroutine`. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned + awaitable object instead. """ http = self.get_auth_http_client() - http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), - functools.partial( - self._on_request_token, self._OAUTH_AUTHENTICATE_URL, - None, callback)) - - @_auth_return_future - def twitter_request(self, path, callback=None, access_token=None, - post_args=None, **args): + response = await http.fetch(self._oauth_request_token_url(callback_uri=callback_uri)) + self._on_request_token(self._OAUTH_AUTHENTICATE_URL, None, response) + + async def twitter_request(self, path, access_token=None, post_args=None, **args): """Fetches the given API path, e.g., ``statuses/user_timeline/btaylor`` The path should not include the format or API version number. @@ -840,10 +680,10 @@ class TwitterMixin(OAuthMixin): .. testoutput:: :hide: - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned + awaitable object instead. """ if path.startswith('http:') or path.startswith('https:'): # Raw urls are useful for e.g. search which doesn't follow the @@ -861,23 +701,13 @@ class TwitterMixin(OAuthMixin): url, access_token, all_args, method=method) args.update(oauth) if args: - url += "?" + urllib_parse.urlencode(args) + url += "?" + urllib.parse.urlencode(args) http = self.get_auth_http_client() - http_callback = functools.partial(self._on_twitter_request, callback, url) if post_args is not None: - fut = http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args)) + response = await http.fetch(url, method="POST", body=urllib.parse.urlencode(post_args)) else: - fut = http.fetch(url) - fut.add_done_callback(http_callback) - - def _on_twitter_request(self, future, url, response_fut): - try: - response = response_fut.result() - except Exception as e: - future.set_exception(AuthError( - "Error response %s fetching %s" % (e, url))) - return - future_set_result_unless_cancelled(future, escape.json_decode(response.body)) + response = await http.fetch(url) + return escape.json_decode(response.body) def _oauth_consumer_token(self): self.require_setting("twitter_consumer_key", "Twitter OAuth") @@ -886,14 +716,13 @@ class TwitterMixin(OAuthMixin): key=self.settings["twitter_consumer_key"], secret=self.settings["twitter_consumer_secret"]) - @gen.coroutine - def _oauth_get_user_future(self, access_token): - user = yield self.twitter_request( + async def _oauth_get_user_future(self, access_token): + user = await self.twitter_request( "/account/verify_credentials", access_token=access_token) if user: user["username"] = user["screen_name"] - raise gen.Return(user) + return user class GoogleOAuth2Mixin(OAuth2Mixin): @@ -920,8 +749,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): _OAUTH_NO_CALLBACKS = False _OAUTH_SETTINGS_KEY = 'google_oauth' - @_auth_return_future - def get_authenticated_user(self, redirect_uri, code, callback): + async def get_authenticated_user(self, redirect_uri, code): """Handles the login for the Google user, returning an access token. The result is a dictionary containing an ``access_token`` field @@ -959,13 +787,12 @@ class GoogleOAuth2Mixin(OAuth2Mixin): .. testoutput:: :hide: - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned awaitable object instead. """ # noqa: E501 http = self.get_auth_http_client() - body = urllib_parse.urlencode({ + body = urllib.parse.urlencode({ "redirect_uri": redirect_uri, "code": code, "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], @@ -973,22 +800,11 @@ class GoogleOAuth2Mixin(OAuth2Mixin): "grant_type": "authorization_code", }) - fut = http.fetch(self._OAUTH_ACCESS_TOKEN_URL, - method="POST", - headers={'Content-Type': 'application/x-www-form-urlencoded'}, - body=body) - fut.add_done_callback(functools.partial(self._on_access_token, callback)) - - def _on_access_token(self, future, response_fut): - """Callback function for the exchange to the access token.""" - try: - response = response_fut.result() - except Exception as e: - future.set_exception(AuthError('Google auth error: %s' % str(e))) - return - - args = escape.json_decode(response.body) - future_set_result_unless_cancelled(future, args) + response = await http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + method="POST", + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + body=body) + return escape.json_decode(response.body) class FacebookGraphMixin(OAuth2Mixin): @@ -998,9 +814,8 @@ class FacebookGraphMixin(OAuth2Mixin): _OAUTH_NO_CALLBACKS = False _FACEBOOK_BASE_URL = "https://graph.facebook.com" - @_auth_return_future - def get_authenticated_user(self, redirect_uri, client_id, client_secret, - code, callback, extra_fields=None): + async def get_authenticated_user(self, redirect_uri, client_id, client_secret, + code, extra_fields=None): """Handles the login for the Facebook user, returning a user object. Example usage: @@ -1042,10 +857,9 @@ class FacebookGraphMixin(OAuth2Mixin): The ``session_expires`` field was updated to support changes made to the Facebook API in March 2017. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned awaitable object instead. """ http = self.get_auth_http_client() args = { @@ -1060,26 +874,14 @@ class FacebookGraphMixin(OAuth2Mixin): if extra_fields: fields.update(extra_fields) - fut = http.fetch(self._oauth_request_token_url(**args)) - fut.add_done_callback(functools.partial(self._on_access_token, redirect_uri, client_id, - client_secret, callback, fields)) - - @gen.coroutine - def _on_access_token(self, redirect_uri, client_id, client_secret, - future, fields, response_fut): - try: - response = response_fut.result() - except Exception as e: - future.set_exception(AuthError('Facebook auth error: %s' % str(e))) - return - + response = await http.fetch(self._oauth_request_token_url(**args)) args = escape.json_decode(response.body) session = { "access_token": args.get("access_token"), "expires_in": args.get("expires_in") } - user = yield self.facebook_request( + user = await self.facebook_request( path="/me", access_token=session["access_token"], appsecret_proof=hmac.new(key=client_secret.encode('utf8'), @@ -1089,8 +891,7 @@ class FacebookGraphMixin(OAuth2Mixin): ) if user is None: - future_set_result_unless_cancelled(future, None) - return + return None fieldmap = {} for field in fields: @@ -1102,11 +903,9 @@ class FacebookGraphMixin(OAuth2Mixin): # This should change in Tornado 5.0. fieldmap.update({"access_token": session["access_token"], "session_expires": str(session.get("expires_in"))}) - future_set_result_unless_cancelled(future, fieldmap) + return fieldmap - @_auth_return_future - def facebook_request(self, path, callback, access_token=None, - post_args=None, **args): + async def facebook_request(self, path, access_token=None, post_args=None, **args): """Fetches the given relative API path, e.g., "/btaylor/picture" If the request is a POST, ``post_args`` should be provided. Query @@ -1153,19 +952,13 @@ class FacebookGraphMixin(OAuth2Mixin): .. versionchanged:: 3.1 Added the ability to override ``self._FACEBOOK_BASE_URL``. - .. deprecated:: 5.1 + .. versionchanged:: 6.0 - The ``callback`` argument is deprecated and will be removed in 6.0. - Use the returned awaitable object instead. + The ``callback`` argument was removed. Use the returned awaitable object instead. """ url = self._FACEBOOK_BASE_URL + path - # Thanks to the _auth_return_future decorator, our "callback" - # argument is a Future, which we cannot pass as a callback to - # oauth2_request. Instead, have oauth2_request return a - # future and chain them together. - oauth_future = self.oauth2_request(url, access_token=access_token, - post_args=post_args, **args) - chain_future(oauth_future, callback) + return await self.oauth2_request(url, access_token=access_token, + post_args=post_args, **args) def _oauth_signature(consumer_token, method, url, parameters={}, token=None): @@ -1173,7 +966,7 @@ def _oauth_signature(consumer_token, method, url, parameters={}, token=None): See http://oauth.net/core/1.0/#signing_process """ - parts = urlparse.urlparse(url) + parts = urllib.parse.urlparse(url) scheme, netloc, path = parts[:3] normalized_url = scheme.lower() + "://" + netloc.lower() + path @@ -1197,7 +990,7 @@ def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None): See http://oauth.net/core/1.0a/#signing_process """ - parts = urlparse.urlparse(url) + parts = urllib.parse.urlparse(url) scheme, netloc, path = parts[:3] normalized_url = scheme.lower() + "://" + netloc.lower() + path @@ -1208,8 +1001,8 @@ def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None): for k, v in sorted(parameters.items()))) base_string = "&".join(_oauth_escape(e) for e in base_elems) - key_elems = [escape.utf8(urllib_parse.quote(consumer_token["secret"], safe='~'))] - key_elems.append(escape.utf8(urllib_parse.quote(token["secret"], safe='~') if token else "")) + key_elems = [escape.utf8(urllib.parse.quote(consumer_token["secret"], safe='~'))] + key_elems.append(escape.utf8(urllib.parse.quote(token["secret"], safe='~') if token else "")) key = b"&".join(key_elems) hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) @@ -1219,7 +1012,7 @@ def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None): def _oauth_escape(val): if isinstance(val, unicode_type): val = val.encode("utf-8") - return urllib_parse.quote(val, safe="~") + return urllib.parse.quote(val, safe="~") def _oauth_parse_response(body): @@ -1227,7 +1020,7 @@ def _oauth_parse_response(body): # have never seen anyone use non-ascii. Leave the response in a byte # string for python 2, and use utf8 on python 3. body = escape.native_str(body) - p = urlparse.parse_qs(body, keep_blank_values=False) + p = urllib.parse.parse_qs(body, keep_blank_values=False) token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0]) # Add the extra parameters the Provider included to the token diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py index 6b3cb16f5..ee5ee7a7b 100644 --- a/tornado/test/auth_test.py +++ b/tornado/test/auth_test.py @@ -6,43 +6,16 @@ from __future__ import absolute_import, division, print_function -import warnings - from tornado.auth import ( - AuthError, OpenIdMixin, OAuthMixin, OAuth2Mixin, + OpenIdMixin, OAuthMixin, OAuth2Mixin, GoogleOAuth2Mixin, FacebookGraphMixin, TwitterMixin, ) -from tornado.concurrent import Future from tornado.escape import json_decode from tornado import gen +from tornado.httpclient import HTTPClientError from tornado.httputil import url_concat -from tornado.log import gen_log -from tornado.testing import AsyncHTTPTestCase, ExpectLog -from tornado.test.util import ignore_deprecation -from tornado.web import RequestHandler, Application, asynchronous, HTTPError - - -class OpenIdClientLoginHandlerLegacy(RequestHandler, OpenIdMixin): - def initialize(self, test): - self._OPENID_ENDPOINT = test.get_url('/openid/server/authenticate') - - with ignore_deprecation(): - @asynchronous - def get(self): - if self.get_argument('openid.mode', None): - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - self.get_authenticated_user( - self.on_user, http_client=self.settings['http_client']) - 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) +from tornado.testing import AsyncHTTPTestCase +from tornado.web import RequestHandler, Application, HTTPError class OpenIdClientLoginHandler(RequestHandler, OpenIdMixin): @@ -58,8 +31,7 @@ class OpenIdClientLoginHandler(RequestHandler, OpenIdMixin): self.finish(user) return res = self.authenticate_redirect() - assert isinstance(res, Future) - assert res.done() + assert res is None class OpenIdServerAuthenticateHandler(RequestHandler): @@ -69,41 +41,6 @@ class OpenIdServerAuthenticateHandler(RequestHandler): self.write('is_valid:true') -class OAuth1ClientLoginHandlerLegacy(RequestHandler, OAuthMixin): - def initialize(self, test, version): - self._OAUTH_VERSION = version - self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token') - self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth1/server/authorize') - self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/oauth1/server/access_token') - - def _oauth_consumer_token(self): - return dict(key='asdf', secret='qwer') - - with ignore_deprecation(): - @asynchronous - def get(self): - if self.get_argument('oauth_token', None): - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - self.get_authenticated_user( - self.on_user, http_client=self.settings['http_client']) - return - res = self.authorize_redirect(http_client=self.settings['http_client']) - assert isinstance(res, Future) - - def on_user(self, user): - if user is None: - raise Exception("user is None") - self.finish(user) - - def _oauth_get_user(self, access_token, callback): - if self.get_argument('fail_in_get_user', None): - raise Exception("failing in get_user") - if access_token != dict(key='uiop', secret='5678'): - raise Exception("incorrect access token %r" % access_token) - callback(dict(email='foo@example.com')) - - class OAuth1ClientLoginHandler(RequestHandler, OAuthMixin): def initialize(self, test, version): self._OAUTH_VERSION = version @@ -180,8 +117,7 @@ class OAuth2ClientLoginHandler(RequestHandler, OAuth2Mixin): def get(self): res = self.authorize_redirect() - assert isinstance(res, Future) - assert res.done() + assert res is None class FacebookClientLoginHandler(RequestHandler, FacebookGraphMixin): @@ -227,21 +163,6 @@ class TwitterClientHandler(RequestHandler, TwitterMixin): return self.settings['http_client'] -class TwitterClientLoginHandlerLegacy(TwitterClientHandler): - with ignore_deprecation(): - @asynchronous - def get(self): - if self.get_argument("oauth_token", None): - self.get_authenticated_user(self.on_user) - return - self.authorize_redirect() - - def on_user(self, user): - if user is None: - raise Exception("user is None") - self.finish(user) - - class TwitterClientLoginHandler(TwitterClientHandler): @gen.coroutine def get(self): @@ -275,7 +196,9 @@ class TwitterClientShowUserHandler(TwitterClientHandler): response = yield self.twitter_request( '/users/show/%s' % self.get_argument('name'), access_token=dict(key='hjkl', secret='vbnm')) - except AuthError: + except HTTPClientError: + # TODO(bdarnell): Should we catch HTTP errors and + # transform some of them (like 403s) into AuthError? self.set_status(500) self.finish('error from twitter request') else: @@ -318,17 +241,12 @@ class AuthTest(AsyncHTTPTestCase): return Application( [ # test endpoints - ('/legacy/openid/client/login', OpenIdClientLoginHandlerLegacy, dict(test=self)), ('/openid/client/login', OpenIdClientLoginHandler, dict(test=self)), - ('/legacy/oauth10/client/login', OAuth1ClientLoginHandlerLegacy, - dict(test=self, version='1.0')), ('/oauth10/client/login', OAuth1ClientLoginHandler, dict(test=self, version='1.0')), ('/oauth10/client/request_params', OAuth1ClientRequestParametersHandler, dict(version='1.0')), - ('/legacy/oauth10a/client/login', OAuth1ClientLoginHandlerLegacy, - dict(test=self, version='1.0a')), ('/oauth10a/client/login', OAuth1ClientLoginHandler, dict(test=self, version='1.0a')), ('/oauth10a/client/login_coroutine', @@ -341,7 +259,6 @@ class AuthTest(AsyncHTTPTestCase): ('/facebook/client/login', FacebookClientLoginHandler, dict(test=self)), - ('/legacy/twitter/client/login', TwitterClientLoginHandlerLegacy, dict(test=self)), ('/twitter/client/login', TwitterClientLoginHandler, dict(test=self)), ('/twitter/client/login_gen_coroutine', TwitterClientLoginGenCoroutineHandler, dict(test=self)), @@ -366,21 +283,6 @@ class AuthTest(AsyncHTTPTestCase): facebook_api_key='test_facebook_api_key', facebook_secret='test_facebook_secret') - def test_openid_redirect_legacy(self): - response = self.fetch('/legacy/openid/client/login', follow_redirects=False) - self.assertEqual(response.code, 302) - self.assertTrue( - '/openid/server/authenticate?' in response.headers['Location']) - - def test_openid_get_user_legacy(self): - response = self.fetch('/legacy/openid/client/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') - response.rethrow() - parsed = json_decode(response.body) - self.assertEqual(parsed["email"], "foo@example.com") - def test_openid_redirect(self): response = self.fetch('/openid/client/login', follow_redirects=False) self.assertEqual(response.code, 302) @@ -396,16 +298,6 @@ class AuthTest(AsyncHTTPTestCase): parsed = json_decode(response.body) self.assertEqual(parsed["email"], "foo@example.com") - def test_oauth10_redirect_legacy(self): - response = self.fetch('/legacy/oauth10/client/login', follow_redirects=False) - self.assertEqual(response.code, 302) - self.assertTrue(response.headers['Location'].endswith( - '/oauth1/server/authorize?oauth_token=zxcv')) - # the cookie is base64('zxcv')|base64('1234') - self.assertTrue( - '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'], - response.headers['Set-Cookie']) - def test_oauth10_redirect(self): response = self.fetch('/oauth10/client/login', follow_redirects=False) self.assertEqual(response.code, 302) @@ -416,16 +308,6 @@ class AuthTest(AsyncHTTPTestCase): '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'], response.headers['Set-Cookie']) - def test_oauth10_get_user_legacy(self): - with ignore_deprecation(): - response = self.fetch( - '/legacy/oauth10/client/login?oauth_token=zxcv', - headers={'Cookie': '_oauth_request_token=enhjdg==|MTIzNA=='}) - response.rethrow() - parsed = json_decode(response.body) - self.assertEqual(parsed['email'], 'foo@example.com') - self.assertEqual(parsed['access_token'], dict(key='uiop', secret='5678')) - def test_oauth10_get_user(self): response = self.fetch( '/oauth10/client/login?oauth_token=zxcv', @@ -444,26 +326,6 @@ class AuthTest(AsyncHTTPTestCase): self.assertTrue('oauth_nonce' in parsed) self.assertTrue('oauth_signature' in parsed) - def test_oauth10a_redirect_legacy(self): - response = self.fetch('/legacy/oauth10a/client/login', follow_redirects=False) - self.assertEqual(response.code, 302) - self.assertTrue(response.headers['Location'].endswith( - '/oauth1/server/authorize?oauth_token=zxcv')) - # the cookie is base64('zxcv')|base64('1234') - self.assertTrue( - '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'], - response.headers['Set-Cookie']) - - def test_oauth10a_get_user_legacy(self): - with ignore_deprecation(): - response = self.fetch( - '/legacy/oauth10a/client/login?oauth_token=zxcv', - headers={'Cookie': '_oauth_request_token=enhjdg==|MTIzNA=='}) - response.rethrow() - parsed = json_decode(response.body) - self.assertEqual(parsed['email'], 'foo@example.com') - self.assertEqual(parsed['access_token'], dict(key='uiop', secret='5678')) - def test_oauth10a_redirect(self): response = self.fetch('/oauth10a/client/login', follow_redirects=False) self.assertEqual(response.code, 302) @@ -524,9 +386,6 @@ class AuthTest(AsyncHTTPTestCase): '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'], response.headers['Set-Cookie']) - def test_twitter_redirect_legacy(self): - self.base_twitter_redirect('/legacy/twitter/client/login') - def test_twitter_redirect(self): self.base_twitter_redirect('/twitter/client/login')