From 1942f97df5d49e20af0440e7b3867e70189149b6 Mon Sep 17 00:00:00 2001 From: Stephen McMillen Date: Wed, 17 Jul 2013 18:59:13 -0400 Subject: [PATCH 1/5] Add in Google OAuth2 support. --- docs/releases/next.rst | 1 + tornado/auth.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/docs/releases/next.rst b/docs/releases/next.rst index d8d516b5..dba53ca9 100644 --- a/docs/releases/next.rst +++ b/docs/releases/next.rst @@ -14,3 +14,4 @@ In Progress while idle. * `.FacebookGraphMixin` has been updated to use the current Facebook login URL, which saves a redirect. +* `.GoogleOAuth2Mixin` has been added so that Google's OAuth2 only apps are able to get a context without OpenID (which uses OAuth 1). \ No newline at end of file diff --git a/tornado/auth.py b/tornado/auth.py index 0cbfa7c0..284c4d1e 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -945,6 +945,88 @@ 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 + + def authorize_redirect(self, redirect_uri=None, client_id=None, + scope=['openid', 'email'], response_type="code", overwrites={}): + """Redirect the user to Google to authenticate them. + + The API itself defaults some of these values that cna be overwritten by the overwrites object. + They are: + * approval_prompt = auto + * access_type = online + """ + extra_params = { + "scope": ' '.join(scope), + "response_type": response_type, + } + + extra_params.update(overwrites) + + OAuth2Mixin.authorize_redirect(self, + self.request.protocol + '://' + self.request.host + (redirect_uri or self.request.uri), + self.settings['google_consumer_key'], + '', extra_params) + + @_auth_return_future + def get_authenticated_user(self, redirect_uri, code, callback): + """Handles the login for the Facebook 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='/auth/google', + code=self.get_argument("code")) + # Save the user with e.g. set_secure_cookie + else: + yield self.authorize_redirect( + redirect_uri='/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.urlencode({ + "redirect_uri": self.request.protocol + '://' + self.request.host + redirect_uri, + "code": code, + "client_id": self.settings['google_consumer_key'], + "client_secret": self.settings['google_consumer_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 = json.loads(escape.native_str(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. From 3b1ae9d07b7bf380c52e60f874a79cb1b37d0454 Mon Sep 17 00:00:00 2001 From: Stephen McMillen Date: Wed, 17 Jul 2013 19:30:24 -0400 Subject: [PATCH 2/5] Fix up documentation for the class and the Sphinx reference. --- docs/auth.rst | 3 +++ tornado/auth.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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/tornado/auth.py b/tornado/auth.py index 284c4d1e..f456124d 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -957,8 +957,8 @@ class GoogleOAuth2Mixin(OAuth2Mixin): The API itself defaults some of these values that cna be overwritten by the overwrites object. They are: - * approval_prompt = auto - * access_type = online + approval_prompt = auto + access_type = online """ extra_params = { "scope": ' '.join(scope), From fd3ea2098d386edcbd416b0dcb1b600268f9d6e7 Mon Sep 17 00:00:00 2001 From: Stephen McMillen Date: Sun, 1 Sep 2013 11:48:46 -0400 Subject: [PATCH 3/5] Rename the object param in GoogleOAuth2Mixin from overwrites to extra_params. --- tornado/auth.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tornado/auth.py b/tornado/auth.py index f456124d..8f28de22 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -952,7 +952,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): _OAUTH_NO_CALLBACKS = False def authorize_redirect(self, redirect_uri=None, client_id=None, - scope=['openid', 'email'], response_type="code", overwrites={}): + scope=['openid', 'email'], response_type="code", extra_params={}): """Redirect the user to Google to authenticate them. The API itself defaults some of these values that cna be overwritten by the overwrites object. @@ -960,12 +960,10 @@ class GoogleOAuth2Mixin(OAuth2Mixin): approval_prompt = auto access_type = online """ - extra_params = { + extra_params.update({ "scope": ' '.join(scope), "response_type": response_type, - } - - extra_params.update(overwrites) + }) OAuth2Mixin.authorize_redirect(self, self.request.protocol + '://' + self.request.host + (redirect_uri or self.request.uri), From 8378225669df32d6530f1a5608a9d22a6bf1fafa Mon Sep 17 00:00:00 2001 From: Stephen McMillen Date: Sun, 1 Sep 2013 12:02:08 -0400 Subject: [PATCH 4/5] Stop making an absolute URL for GoogleOAuth2Mixin --- tornado/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tornado/auth.py b/tornado/auth.py index 8f28de22..c88c6888 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -966,7 +966,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): }) OAuth2Mixin.authorize_redirect(self, - self.request.protocol + '://' + self.request.host + (redirect_uri or self.request.uri), + (redirect_uri or self.request.uri), self.settings['google_consumer_key'], '', extra_params) From 13c149a596d6c5bc68a80ce2cf508b11ee5d2555 Mon Sep 17 00:00:00 2001 From: Stephen McMillen Date: Wed, 4 Sep 2013 08:41:24 -0400 Subject: [PATCH 5/5] Clean up GoogleOAuth2Mixin, fold scope and response_type into the base OAuth2Mixin --- tornado/auth.py | 45 +++++++++++++++------------------------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/tornado/auth.py b/tornado/auth.py index c88c6888..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() @@ -950,29 +954,11 @@ class GoogleOAuth2Mixin(OAuth2Mixin): _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 - - def authorize_redirect(self, redirect_uri=None, client_id=None, - scope=['openid', 'email'], response_type="code", extra_params={}): - """Redirect the user to Google to authenticate them. - - The API itself defaults some of these values that cna be overwritten by the overwrites object. - They are: - approval_prompt = auto - access_type = online - """ - extra_params.update({ - "scope": ' '.join(scope), - "response_type": response_type, - }) - - OAuth2Mixin.authorize_redirect(self, - (redirect_uri or self.request.uri), - self.settings['google_consumer_key'], - '', extra_params) + _OAUTH_SETTINGS_KEY = 'google_oauth' @_auth_return_future def get_authenticated_user(self, redirect_uri, code, callback): - """Handles the login for the Facebook user, returning a user object. + """Handles the login for the Google user, returning a user object. Example usage:: @@ -982,23 +968,23 @@ class GoogleOAuth2Mixin(OAuth2Mixin): def get(self): if self.get_argument("code", False): user = yield self.get_authenticated_user( - redirect_uri='/auth/google', + 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='/auth/google', + 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.urlencode({ - "redirect_uri": self.request.protocol + '://' + self.request.host + redirect_uri, + body = urllib_parse.urlencode({ + "redirect_uri": redirect_uri, "code": code, - "client_id": self.settings['google_consumer_key'], - "client_secret": self.settings['google_consumer_secret'], + "client_id": self.settings[self._OAUTH_SETTINGS_KEY]['key'], + "client_secret": self.settings[self._OAUTH_SETTINGS_KEY]['secret'], "grant_type": "authorization_code", }) @@ -1012,8 +998,7 @@ class GoogleOAuth2Mixin(OAuth2Mixin): future.set_exception(AuthError('Google auth error: %s' % str(response))) return - args = json.loads(escape.native_str(response.body)) - + args = escape.json_decode(response.body) future.set_result(args) def get_auth_http_client(self):