Repository: tornadoweb/tornado

Issue:
Need more parameters for auth get_authenticated_user method
Google OAuth2 has changed the parameters to be passed to change the code to access_token. 

params = { 'code':code,
                               'client_id':self.settings['google_oauth']['key'],
                                'client_secret':self.settings['google_oauth']['secret'],
                                'redirect_uri':'http://localhost',
                                'grant_type':'authorization_code',
                                 }

As these params cant be passed in the current get_authenticated_user method, OAuth2 method fails to get access_token. 

I have used to requests to get the token and it works fine. 

Extra params option is coded in authorize_redirect , so it can be added in get_authenticated_user

I havent tested for Facebook/Twitter, get_authenticated_user method for FacebookGraphMixin has extra fields and option to pass client_id and client_secret. 

Same can be added for get_authenticated_user in GoogleOAuth2Mixin
Suggestion: add facebook_redirect_uri in settings
So we can use the following and independent code in FacebookGraphMixin:

```
        user_data = yield self.get_authenticated_user(
            redirect_uri=self.settings['facebook_redirect_uri']
            client_id=self.settings["facebook_api_key"],
            client_secret=self.settings["facebook_secret"],
            code=self.get_argument("code"))
```



Discussion:
I don't understand what change you are suggesting. These five arguments are already what we use:

https://github.com/tornadoweb/tornado/blob/7cb24cfbe1bf5bf9aeebdbffe856ac5e67c4a7fb/tornado/auth.py#L899-L905
When I try to authenticate using get_authenticated_user , google reply throws error. I will post the parameters passed and error. 
Hi @rajasankar can you please see if this error is still happening, and post the request and error, so I can reproduce this error and fix it? Thanks!
I have followed the standard tutorial steps given in auth.py , get_authenticated_user and authorize_redirect .

It seems that "grant_type" needs to be included for OAuth2 version 4. Tornado hardcoded the params for v1. 

If you try with OAuth2 v4, default methods wont work. I have used requests framework as a workaround. My full code below. 

```
                code = self.get_argument( 'code' )
                headers = {'Content-Type': 'application/x-www-form-urlencoded'}
                params = {
                                'code':code,
                                'client_id':self.settings['google_oauth']['key'],
                                'client_secret':self.settings['google_oauth']['secret'],
                                'redirect_uri':'http://localhost:9999/auth',
                                'grant_type':'authorization_code',
                                 }
                url = 'https://www.googleapis.com/oauth2/v4/token'
                reply = requests.post( url, headers = headers, params = params, )
                self.set_secure_cookie( "user", tornado.escape.json_encode( reply.json()['access_token'] ), expires_days = 7, httponly = True )
                self.redirect( '/' )

```
Error message is the standard error message from Google when incorrect auth is tried. 


Thanks a lot for the extra information, it's very useful, will be working on it later.
Hi. Are there any news regarding this? 

I'm having a similar issue trying to use google oauth methods (more specifically get_authenticated_user).

This code specifically:

`user = await self.get_authenticated_user(redirect_uri='http://localhost:3000',code=code)`

Returns:

```
    HTTPServerRequest(protocol='https', host='localhost:8888', method='GET', uri='/api/oauth/google/<auth_code>', version='HTTP/1.1', remote_ip='127.0.0.1')
    Traceback (most recent call last):
      File "....", line 1699, in _execute
        result = await result
      File "C:\...\api\routes\GoogleOauth.py", line 13, in get
        user = await self.get_authenticated_user(redirect_uri='http://localhost:3000',code=code)
      File "C:\<>\Python36\lib\site-packages\tornado\auth.py", line 914, in get_authenticated_user
        body=body,
    tornado.httpclient.HTTPClientError: HTTP 400: Bad Request
```
I haven't personally used the OAuth stuff in years; someone who does will need to debug this. It's still not clear from this thread what the issue is; a comment says `grant_type` is required, but as my first message shows it's already there. 
I was able to reproduce this bug today. I was using request's full_url() for the redirect_uri and it somehow was returning wrong values. After I used the redirect_uri as an static value everything worked correctly.
You can do this today:  Application settings don't have to be predeclared in any way, so if you want to set facebook_redirect_uri and use it as you've described, you can.  You don't even have to put your api key and secret in settings for FacebookGraphMixin if you don't want to (the examples show it this way for consistency with the older FacebookMixin and its twitter counterpart)

I am doing this. So, at least it would be a good idea to include this in the documentation of FacebookGraphMixin, since it is a configuration directly related with the Facebook App settings.

I was going to make this change but I noticed that we probably shouldn't even be passing in redirect_uri at all - it should be picked up from self.request.uri (by default) like it is in the other mixins.  And is redirect_uri required for get_authenticated_user (which doesn't actually do any redirects)?  I'm not familiar enough with FacebookGraphMixin to know what exactly to do here; can someone who's used it more provide some guidance?


Resolving patch:
diff --git a/demos/google_auth/main.py b/demos/google_auth/main.py
new file mode 100644
--- /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/demos/twitter/twitterdemo.py b/demos/twitter/twitterdemo.py
deleted file mode 100755
--- a/demos/twitter/twitterdemo.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env python
-"""A simplistic Twitter viewer to demonstrate the use of TwitterMixin.
-
-To run this app, you must first register an application with Twitter:
-  1) Go to https://dev.twitter.com/apps and create an application.
-     Your application must have a callback URL registered with Twitter.
-     It doesn't matter what it is, but it has to be there (Twitter won't
-     let you use localhost in a registered callback URL, but that won't stop
-     you from running this demo on localhost).
-  2) Create a file called "secrets.cfg" and put your consumer key and
-     secret (which Twitter gives you when you register an app) in it:
-       twitter_consumer_key = 'asdf1234'
-       twitter_consumer_secret = 'qwer5678'
-     (you could also generate a random value for "cookie_secret" and put it
-     in the same file, although it's not necessary to run this demo)
-  3) Run this program and go to http://localhost:8888 (by default) in your
-     browser.
-"""
-
-import asyncio
-import logging
-
-from tornado.auth import TwitterMixin
-from tornado.escape import json_decode, json_encode
-from tornado import gen
-from tornado.options import define, options, parse_command_line, parse_config_file
-from tornado.web import Application, RequestHandler, authenticated
-
-define("port", default=8888, help="port to listen on")
-define(
-    "config_file", default="secrets.cfg", help="filename for additional configuration"
-)
-
-define(
-    "debug",
-    default=False,
-    group="application",
-    help="run in debug mode (with automatic reloading)",
-)
-# The following settings should probably be defined in secrets.cfg
-define("twitter_consumer_key", type=str, group="application")
-define("twitter_consumer_secret", type=str, group="application")
-define(
-    "cookie_secret",
-    type=str,
-    group="application",
-    default="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE__",
-    help="signing key for secure cookies",
-)
-
-
-class BaseHandler(RequestHandler):
-    COOKIE_NAME = "twitterdemo_user"
-
-    def get_current_user(self):
-        user_json = self.get_signed_cookie(self.COOKIE_NAME)
-        if not user_json:
-            return None
-        return json_decode(user_json)
-
-
-class MainHandler(BaseHandler, TwitterMixin):
-    @authenticated
-    @gen.coroutine
-    def get(self):
-        timeline = yield self.twitter_request(
-            "/statuses/home_timeline", access_token=self.current_user["access_token"]
-        )
-        self.render("home.html", timeline=timeline)
-
-
-class LoginHandler(BaseHandler, TwitterMixin):
-    @gen.coroutine
-    def get(self):
-        if self.get_argument("oauth_token", None):
-            user = yield self.get_authenticated_user()
-            del user["description"]
-            self.set_signed_cookie(self.COOKIE_NAME, json_encode(user))
-            self.redirect(self.get_argument("next", "/"))
-        else:
-            yield self.authorize_redirect(callback_uri=self.request.full_url())
-
-
-class LogoutHandler(BaseHandler):
-    def get(self):
-        self.clear_cookie(self.COOKIE_NAME)
-
-
-async def main():
-    parse_command_line(final=False)
-    parse_config_file(options.config_file)
-
-    app = Application(
-        [("/", MainHandler), ("/login", LoginHandler), ("/logout", LogoutHandler)],
-        login_url="/login",
-        **options.group_dict("application")
-    )
-    app.listen(options.port)
-
-    logging.info("Listening on http://localhost:%d" % options.port)
-    await asyncio.Event().wait()
-
-
-if __name__ == "__main__":
-    asyncio.run(main())
diff --git a/tornado/auth.py b/tornado/auth.py
--- a/tornado/auth.py
+++ b/tornado/auth.py
@@ -33,23 +33,39 @@
 
 Example usage for Google OAuth:
 
+.. testsetup::
+
+    import urllib
+
 .. testcode::
 
     class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
-                                   tornado.auth.GoogleOAuth2Mixin):
+                                    tornado.auth.GoogleOAuth2Mixin):
         async def get(self):
-            if self.get_argument('code', False):
-                user = await self.get_authenticated_user(
-                    redirect_uri='http://your.site.com/auth/google',
-                    code=self.get_argument('code'))
-                # Save the user with e.g. set_signed_cookie
-            else:
-                self.authorize_redirect(
-                    redirect_uri='http://your.site.com/auth/google',
-                    client_id=self.settings['google_oauth']['key'],
-                    scope=['profile', 'email'],
-                    response_type='code',
-                    extra_params={'approval_prompt': 'auto'})
+            # 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=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. 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=redirect_uri,
+                        client_id=self.get_google_oauth_settings()['key'],
+                        scope=['profile', 'email'],
+                        response_type='code',
+                        extra_params={'approval_prompt': 'auto'})
 
 .. testoutput::
    :hide:
@@ -63,6 +79,7 @@ async def get(self):
 import time
 import urllib.parse
 import uuid
+import warnings
 
 from tornado import httpclient
 from tornado import escape
@@ -571,7 +588,13 @@ def authorize_redirect(
 
            The ``callback`` argument and returned awaitable were removed;
            this is now an ordinary synchronous function.
+
+        .. deprecated:: 6.4
+           The ``client_secret`` argument (which has never had any effect)
+           is deprecated and will be removed in Tornado 7.0.
         """
+        if client_secret is not None:
+            warnings.warn("client_secret argument is deprecated", DeprecationWarning)
         handler = cast(RequestHandler, self)
         args = {"response_type": response_type}
         if redirect_uri is not None:
@@ -705,6 +728,12 @@ async def get(self):
     includes the attributes ``username``, ``name``, ``access_token``,
     and all of the custom Twitter user attributes described at
     https://dev.twitter.com/docs/api/1.1/get/users/show
+
+    .. deprecated:: 6.3
+       This class refers to version 1.1 of the Twitter API, which has been
+       deprecated by Twitter. Since Twitter has begun to limit access to its
+       API, this class will no longer be updated and will be removed in the
+       future.
     """
 
     _OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"
@@ -839,12 +868,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
     """
@@ -890,27 +925,39 @@ async def get_authenticated_user(
 
         Example usage:
 
+        .. testsetup::
+
+            import urllib
+
         .. testcode::
 
             class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
                                            tornado.auth.GoogleOAuth2Mixin):
                 async def get(self):
-                    if self.get_argument('code', False):
-                        access = await self.get_authenticated_user(
-                            redirect_uri='http://your.site.com/auth/google',
-                            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.
-                    else:
-                        self.authorize_redirect(
-                            redirect_uri='http://your.site.com/auth/google',
-                            client_id=self.get_google_oauth_settings()['key'],
-                            scope=['profile', 'email'],
-                            response_type='code',
-                            extra_params={'approval_prompt': 'auto'})
+                    # 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=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. 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=redirect_uri,
+                                client_id=self.get_google_oauth_settings()['key'],
+                                scope=['profile', 'email'],
+                                response_type='code',
+                                extra_params={'approval_prompt': 'auto'})
 
         .. testoutput::
            :hide:
@@ -971,18 +1018,21 @@ async def get_authenticated_user(
             class FacebookGraphLoginHandler(tornado.web.RequestHandler,
                                             tornado.auth.FacebookGraphMixin):
               async def get(self):
-                  if self.get_argument("code", False):
-                      user = await self.get_authenticated_user(
-                          redirect_uri='/auth/facebookgraph/',
-                          client_id=self.settings["facebook_api_key"],
-                          client_secret=self.settings["facebook_secret"],
-                          code=self.get_argument("code"))
-                      # Save the user with e.g. set_signed_cookie
-                  else:
-                      self.authorize_redirect(
-                          redirect_uri='/auth/facebookgraph/',
-                          client_id=self.settings["facebook_api_key"],
-                          extra_params={"scope": "read_stream,offline_access"})
+                redirect_uri = urllib.parse.urljoin(
+                    self.application.settings['redirect_base_uri'],
+                    self.reverse_url('facebook_oauth'))
+                if self.get_argument("code", False):
+                    user = await self.get_authenticated_user(
+                        redirect_uri=redirect_uri,
+                        client_id=self.settings["facebook_api_key"],
+                        client_secret=self.settings["facebook_secret"],
+                        code=self.get_argument("code"))
+                    # Save the user with e.g. set_signed_cookie
+                else:
+                    self.authorize_redirect(
+                        redirect_uri=redirect_uri,
+                        client_id=self.settings["facebook_api_key"],
+                        extra_params={"scope": "user_posts"})
 
         .. testoutput::
            :hide:


Test patch:
diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py
--- a/tornado/test/auth_test.py
+++ b/tornado/test/auth_test.py
@@ -550,7 +550,6 @@ def get(self):
             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"},