From: Ben Darnell Date: Fri, 7 Jul 2023 23:37:45 +0000 (-0400) Subject: demos: Add a demo app for google auth X-Git-Tag: v6.4.0b1~24^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a57189bf9d6790a156ccdf16900a12b16174f972;p=thirdparty%2Ftornado.git demos: Add a demo app for google auth Add some more detail to app registration docs. This was done mainly to verify that we don't need to introduce new parameters as requested in #2140 Closes #2140 --- diff --git a/demos/google_auth/.gitignore b/demos/google_auth/.gitignore new file mode 100644 index 000000000..5cfc307c0 --- /dev/null +++ b/demos/google_auth/.gitignore @@ -0,0 +1 @@ +main.cfg diff --git a/demos/google_auth/main.py b/demos/google_auth/main.py new file mode 100644 index 000000000..06dd3b5cd --- /dev/null +++ b/demos/google_auth/main.py @@ -0,0 +1,113 @@ +"""Demo app for GoogleOAuth2Mixin + +Recommended usage: +- Register an app with Google following the instructions at + https://www.tornadoweb.org/en/stable/auth.html#tornado.auth.GoogleOAuth2Mixin +- Use "http://localhost:8888/auth/google" as the redirect URI. +- Create a file in this directory called main.cfg, containing two lines (python syntax): + google_oauth_key="..." + google_oauth_secret="..." +- Run this file with `python main.py --config_file=main.cfg` +- Visit "http://localhost:8888" in your browser. +""" +import asyncio +import json +import tornado +import urllib.parse + +from tornado.options import define, options +from tornado.web import url + +define("port", default=8888, help="run on the given port", type=int) +define("google_oauth_key", help="Google OAuth Key") +define("google_oauth_secret", help="Google OAuth Secret") +define( + "config_file", + help="tornado config file", + callback=lambda path: tornado.options.parse_config_file(path, final=False), +) + + +class BaseHandler(tornado.web.RequestHandler): + def get_current_user(self): + user_cookie = self.get_signed_cookie("googledemo_user") + if user_cookie: + return json.loads(user_cookie) + return None + + +class IndexHandler(BaseHandler, tornado.auth.GoogleOAuth2Mixin): + @tornado.web.authenticated + async def get(self): + try: + # This is redundant: we got the userinfo in the login handler. + # But this demonstrates the usage of oauth2_request outside of + # the login flow, and getting anything more than userinfo + # leads to more approval prompts and complexity. + user_info = await self.oauth2_request( + "https://www.googleapis.com/oauth2/v1/userinfo", + access_token=self.current_user["access_token"], + ) + except tornado.httpclient.HTTPClientError as e: + print(e.response.body) + raise + self.write(f"Hello {user_info['name']}") + + +class LoginHandler(BaseHandler, tornado.auth.GoogleOAuth2Mixin): + async def get(self): + redirect_uri = urllib.parse.urljoin( + self.application.settings["redirect_base_uri"], + self.reverse_url("google_oauth"), + ) + if self.get_argument("code", False): + access = await self.get_authenticated_user( + redirect_uri=redirect_uri, code=self.get_argument("code") + ) + user = await self.oauth2_request( + "https://www.googleapis.com/oauth2/v1/userinfo", + access_token=access["access_token"], + ) + # Save the user and access token. + user_cookie = dict(id=user["id"], access_token=access["access_token"]) + self.set_signed_cookie("googledemo_user", json.dumps(user_cookie)) + self.redirect("/") + else: + self.authorize_redirect( + redirect_uri=redirect_uri, + client_id=self.get_google_oauth_settings()["key"], + scope=["profile", "email"], + response_type="code", + extra_params={"approval_prompt": "auto"}, + ) + + +class LogoutHandler(BaseHandler): + def get(self): + self.clear_cookie("user") + self.redirect("/") + + +async def main(): + tornado.options.parse_command_line() + app = tornado.web.Application( + [ + url(r"/", IndexHandler), + url(r"/auth/google", LoginHandler, name="google_oauth"), + url(r"/logout", LogoutHandler), + ], + redirect_base_uri=f"http://localhost:{options.port}", + google_oauth=dict( + key=options.google_oauth_key, secret=options.google_oauth_secret + ), + debug=True, + cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", + login_url="/auth/google", + ) + app.listen(options.port) + shutdown_event = asyncio.Event() + await shutdown_event.wait() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tornado/auth.py b/tornado/auth.py index e9e3c359f..7f05a531f 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -852,12 +852,18 @@ class GoogleOAuth2Mixin(OAuth2Mixin): * Go to the Google Dev Console at http://console.developers.google.com * Select a project, or create a new one. + * Depending on permissions required, you may need to set your app to + "testing" mode and add your account as a test user, or go through + a verfication process. You may also need to use the "Enable + APIs and Services" command to enable specific services. * In the sidebar on the left, select Credentials. * Click CREATE CREDENTIALS and click OAuth client ID. * Under Application type, select Web application. * Name OAuth 2.0 client and click Create. * Copy the "Client secret" and "Client ID" to the application settings as ``{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}`` + * You must register the ``redirect_uri`` you plan to use with this class + on the Credentials page. .. versionadded:: 3.2 """ @@ -907,19 +913,26 @@ class GoogleOAuth2Mixin(OAuth2Mixin): class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, tornado.auth.GoogleOAuth2Mixin): + # Google requires an exact match for redirect_uri, so it's + # best to get it from your app configuration instead of from + # self.request.full_uri(). + redirect_uri = urllib.parse.urljoin(self.application.settings['redirect_base_uri'], + self.reverse_url('google_oauth')) async def get(self): if self.get_argument('code', False): access = await self.get_authenticated_user( - redirect_uri='http://your.site.com/auth/google', + redirect_uri=redirect_uri, code=self.get_argument('code')) user = await self.oauth2_request( "https://www.googleapis.com/oauth2/v1/userinfo", access_token=access["access_token"]) - # Save the user and access token with - # e.g. set_signed_cookie. + # Save the user and access token. For example: + user_cookie = dict(id=user["id"], access_token=access["access_token"]) + self.set_signed_cookie("user", json.dumps(user_cookie)) + self.redirect("/") else: self.authorize_redirect( - redirect_uri='http://your.site.com/auth/google', + redirect_uri=redirect_uri, client_id=self.get_google_oauth_settings()['key'], scope=['profile', 'email'], response_type='code',