]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
demos: Add a demo app for google auth
authorBen Darnell <ben@bendarnell.com>
Fri, 7 Jul 2023 23:37:45 +0000 (19:37 -0400)
committerBen Darnell <ben@bendarnell.com>
Sat, 8 Jul 2023 00:38:00 +0000 (20:38 -0400)
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

demos/google_auth/.gitignore [new file with mode: 0644]
demos/google_auth/main.py [new file with mode: 0644]
tornado/auth.py

diff --git a/demos/google_auth/.gitignore b/demos/google_auth/.gitignore
new file mode 100644 (file)
index 0000000..5cfc307
--- /dev/null
@@ -0,0 +1 @@
+main.cfg
diff --git a/demos/google_auth/main.py b/demos/google_auth/main.py
new file mode 100644 (file)
index 0000000..06dd3b5
--- /dev/null
@@ -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())
index e9e3c359f3b60667710bbdf4664404184051f1f2..7f05a531f10dca40cce45334d1d7a2f560ea44e4 100644 (file)
@@ -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',