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):
+ """Fetches the given URL auth an OAuth2 access token.
+
+ If the request is a POST, ``post_args`` should be provided. Query
+ string arguments should be given as keyword arguments.
+
+ Example usage:
+
+ ..testcode::
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.FacebookGraphMixin):
+ @tornado.web.authenticated
+ @tornado.gen.coroutine
+ def get(self):
+ new_entry = yield self.oauth2_request(
+ "https://graph.facebook.com/me/feed",
+ post_args={"message": "I am posting from my Tornado application!"},
+ 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:
+
+ .. versionadded:: 4.3
+ """
+ all_args = {}
+ if access_token:
+ all_args["access_token"] = access_token
+ all_args.update(args)
+
+ if all_args:
+ url += "?" + urllib_parse.urlencode(all_args)
+ callback = functools.partial(self._on_facebook_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_oauth2_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 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.
+
+ .. versionadded:: 4.3
+ """
+ return httpclient.AsyncHTTPClient()
+
class TwitterMixin(OAuthMixin):
"""Twitter OAuth authentication.
@_auth_return_future
def get_authenticated_user(self, redirect_uri, code, callback):
- """Handles the login for the Google user, returning a user object.
+ """Handles the login for the Google user, returning an access token.
+
+ The result is a dictionary containing an ``access_token`` field
+ ([among others](https://developers.google.com/identity/protocols/OAuth2WebServer#handlingtheresponse)).
+ Unlike other ``get_authenticated_user`` methods in this package,
+ this method does not return any additional information about the user.
+ The returned access token can be used with `OAuth2Mixin.oauth2_request`
+ to request additional information (perhaps from
+ ``https://www.googleapis.com/oauth2/v2/userinfo``)
Example usage:
args = escape.json_decode(response.body)
future.set_result(args)
- 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."""
The given path is relative to ``self._FACEBOOK_BASE_URL``,
by default "https://graph.facebook.com".
+ This method is a wrapper around `OAuth2Mixin.oauth2_request`;
+ the only difference is that this method takes a relative path,
+ while ``oauth2_request`` takes a complete url.
+
.. versionchanged:: 3.1
Added the ability to override ``self._FACEBOOK_BASE_URL``.
"""
url = self._FACEBOOK_BASE_URL + path
- all_args = {}
- if access_token:
- all_args["access_token"] = access_token
- all_args.update(args)
-
- if all_args:
- url += "?" + urllib_parse.urlencode(all_args)
- callback = functools.partial(self._on_facebook_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_facebook_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 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()
+ return self.oauth2_request(url, callback, access_token,
+ post_args, **args)
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
from __future__ import absolute_import, division, print_function, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin, AuthError, GoogleOAuth2Mixin
from tornado.concurrent import Future
from tornado.escape import json_decode
from tornado import gen
+from tornado.httputil import url_concat
from tornado.log import gen_log
from tornado.testing import AsyncHTTPTestCase, ExpectLog
from tornado.util import u
response = self.fetch('/twitter/client/show_user_future?name=error')
self.assertEqual(response.code, 500)
self.assertIn(b'Error response HTTP 500', response.body)
+
+
+class GoogleLoginHandler(RequestHandler, GoogleOAuth2Mixin):
+ def initialize(self, test):
+ self._OAUTH_REDIRECT_URI = test.get_url('/client/login')
+ self._OAUTH_AUTHORIZE_URL = test.get_url('/google/oauth2/authorize')
+ self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/google/oauth2/token')
+
+ @gen.coroutine
+ def get(self):
+ code = self.get_argument('code', None)
+ if code is not None:
+ # retrieve authenticate google user
+ user = yield self.get_authenticated_user(self._OAUTH_REDIRECT_URI,
+ code)
+ # return the user as json
+ self.write(user)
+ else:
+ yield self.authorize_redirect(
+ redirect_uri=self._OAUTH_REDIRECT_URI,
+ client_id=self.settings['google_oauth']['key'],
+ client_secret=self.settings['google_oauth']['secret'],
+ scope=['profile', 'email'],
+ response_type='code',
+ extra_params={'prompt': 'select_account'})
+
+
+class GoogleOAuth2AuthorizeHandler(RequestHandler):
+ def get(self):
+ # issue a fake auth code and redirect to redirect_uri
+ code = 'fake-authorization-code'
+ self.redirect(url_concat(self.get_argument('redirect_uri'),
+ dict(code=code)))
+
+
+class GoogleOAuth2TokenHandler(RequestHandler):
+ def post(self):
+ assert self.get_argument('code') == 'fake-authorization-code'
+ # issue a fake token
+ self.finish({
+ 'access_token': 'fake-access-token',
+ 'expires_in': 'never-expires'
+ })
+
+
+class GoogleOAuth2Test(AsyncHTTPTestCase):
+ def get_app(self):
+ return Application(
+ [
+ # test endpoints
+ ('/client/login', GoogleLoginHandler, dict(test=self)),
+
+ # simulated google authorization server endpoints
+ ('/google/oauth2/authorize', GoogleOAuth2AuthorizeHandler),
+ ('/google/oauth2/token', GoogleOAuth2TokenHandler),
+ ],
+ google_oauth={
+ "key": 'fake_google_client_id',
+ "secret": 'fake_google_client_secret'
+ })
+
+ def test_google_login(self):
+ response = self.fetch('/client/login')
+ self.assertDictEqual({
+ u('access_token'): u('fake-access-token'),
+ u('expires_in'): u('never-expires'),
+ }, json_decode(response.body))