diff --git a/docs/auth.rst b/docs/auth.rst index c1f7f8ad..dd2ca554 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -31,6 +31,9 @@ .. autoclass:: GoogleMixin :members: + + .. autoclass:: GoogleOAuth2Mixin + :members: Facebook -------- diff --git a/docs/releases/next.rst b/docs/releases/next.rst index 7bd7768b..f64a0289 100644 --- a/docs/releases/next.rst +++ b/docs/releases/next.rst @@ -31,3 +31,4 @@ In Progress arguments, just like `~.RequestHandler.clear_cookie`. * The embedded ``ca-certificats.crt`` file has been updated with the current Mozilla CA list. +* `.GoogleOAuth2Mixin` has been added so that Google's OAuth2 only apps are able to get a context without OpenID (which uses OAuth 1). diff --git a/tornado/auth.py b/tornado/auth.py index 0cbfa7c0..5f8aec9e 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -548,7 +548,8 @@ class OAuth2Mixin(object): """ @return_future def authorize_redirect(self, redirect_uri=None, client_id=None, - client_secret=None, extra_params=None, + client_secret=None, scope=None, + response_type="code", extra_params=None, callback=None): """Redirects the user to obtain OAuth authorization for this service. @@ -566,10 +567,13 @@ class OAuth2Mixin(object): """ args = { "redirect_uri": redirect_uri, - "client_id": client_id + "client_id": client_id, + "response_type": response_type } if extra_params: args.update(extra_params) + if scope: + args['scope'] = ' '.join(scope) self.redirect( url_concat(self._OAUTH_AUTHORIZE_URL, args)) callback() @@ -945,6 +949,67 @@ class GoogleMixin(OpenIdMixin, OAuthMixin): return OpenIdMixin.get_authenticated_user(self) +class GoogleOAuth2Mixin(OAuth2Mixin): + """Google authentication using OAuth2.""" + _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth" + _OAUTH_ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" + _OAUTH_NO_CALLBACKS = False + _OAUTH_SETTINGS_KEY = 'google_oauth' + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Google user, returning a user object. + + Example usage:: + + class GoogleOAuth2LoginHandler(LoginHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.asynchronous + @tornado.gen.coroutine + def get(self): + 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_consumer_key"], + scope=['openid', 'email'], + response_type='code', + extra_params={"approval_prompt": "auto"}) + """ + http = self.get_auth_http_client() + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, + "code": code, + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], + "grant_type": "authorization_code", + }) + + http.fetch(self._OAUTH_ACCESS_TOKEN_URL, + self.async_callback(self._on_access_token, callback), + method="POST", headers={'Content-Type': 'application/x-www-form-urlencoded'}, body=body) + + def _on_access_token(self, future, response): + """Callback function for the exchange to the access token.""" + if response.error: + future.set_exception(AuthError('Google auth error: %s' % str(response))) + return + + 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 FacebookMixin(object): """Facebook Connect authentication.