'Google OAuth 2.0 failing with Error 400: invalid_request for some client_id, but works well for others in the same project

We have some apps (or maybe we should call them a handful of scripts) that use Google APIs to facilitate some administrative tasks. Recently, after making another client_id in the same project, I started getting an error message similar to the one described in localhost redirect_uri does not work for Google Oauth2 (results in 400: invalid_request error). I.e.,

Error 400: invalid_request

You can't sign in to this app because it doesn't comply with Google's OAuth 2.0 policy for keeping apps secure.

You can let the app developer know that this app doesn't comply with one or more Google validation rules.

Request details:

The content in this section has been provided by the app developer. This content has not been reviewed or verified by Google.

If you’re the app developer, make sure that these request details comply with Google policies.

redirect_uri: urn:ietf:wg:oauth:2.0:oob

How do I get through this error? It is important to note that:

  • The OAuth consent screen for this project is marked as "Internal". Therefore any mentions of Google review of the project, or publishing status are irrelevant
  • I do have "Trust internal, domain-owned apps" enabled for the domain
  • Another client id in the same project works and there are no obvious differences between the client IDs - they are both "Desktop" type which only gives me a Client ID and Client secret that are different
  • This is a command line script, so I use the "copy/paste" verification method as documented here hence the urn:ietf:wg:oauth:2.0:oob redirect URI (copy/paste is the only friendly way to run this on a headless machine which has no browser).
  • I was able to reproduce the same problem in a dev domain. I have three client ids. The oldest one is from January 2021, another one from December 2021, and one I created today - March 2022. Of those, only the December 2021 works and lets me choose which account to authenticate with before it either accepts it or rejects it with "Error 403: org_internal" (this is expected). The other two give me an "Error 400: invalid_request" and do not even let me choose the "internal" account. Here are the URLs generated by my app (I use the ruby google client APIs) and the only difference between them is the client_id - January 2021, December 2021, March 2022.

Here is the part of the code around the authorization flow, and the URLs for the different client IDs are what was produced on the $stderr.puts url line. It is pretty much the same thing as documented in the official example here (version as of this writing).


OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'

def user_credentials_for(scope, user_id = 'default')
    token_store = Google::Auth::Stores::FileTokenStore.new(:file => token_store_path)
    authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store)
    credentials = authorizer.get_credentials(user_id)
    if credentials.nil?
        url = authorizer.get_authorization_url(base_url: OOB_URI)
        $stderr.puts ""
        $stderr.puts "-----------------------------------------------"
        $stderr.puts "Requesting authorization for '#{user_id}'"
        $stderr.puts "Open the following URL in your browser and authorize the application."
        $stderr.puts url
        code = $stdin.readline.chomp
        $stderr.puts "-----------------------------------------------"
        credentials = authorizer.get_and_store_credentials_from_code(
            user_id: user_id, code: code, base_url: OOB_URI)
    end
    credentials
end
                                                                                                                                          


Solution 1:[1]

I sent off an email to someone on the Google OAuth team. This is the gist of their response.

As I feared your issue is related to Making Google OAuth interactions safer by using more secure OAuth flows

The current recommendation from google is to move to use localhost/loopback redirects as recommended here: instructions-oob or use the OAuth for devices flow if you are using non-sensitive scopes and need a headless solution.

Solution 2:[2]

Here is a cringy workaround for this situation:

Replace urn:ietf:wg:oauth:2.0:oob with http://localhost:1/ in the code posted in the question. This makes the flow go through, my browser gets redirected and fails and I get an error messages like:

This site can’t be reached

The webpage at http://localhost:1/oauth2callback?
code=4/a3MU9MlhWxit8P7N8QsGtT0ye8GJygOeCa3MU9MlhWxit8P7N8QsGtT0y
e8GJygOeC&scope=email%20profile%20https... might be temporarily
down or it may have moved permanently to a new web address.

ERR_UNSAFE_PORT

Now copy the code code value from the failing URL, paste it into the app, and voila... same as before :)

P.S. Here is the updated "working" version:


def user_credentials_for(scope, user_id = 'default')
    token_store = Google::Auth::Stores::FileTokenStore.new(:file => token_store_path)
    authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store, "http://localhost:1/")
    credentials = authorizer.get_credentials(user_id)
    if credentials.nil?
        url = authorizer.get_authorization_url
        $stderr.puts ""
        $stderr.puts "-----------------------------------------------"
        $stderr.puts "Requesting authorization for '#{user_id}'"
        $stderr.puts "Open the following URL in your browser and authorize the application."
        $stderr.puts url
        $stderr.puts
        $stderr.puts "At the end the browser will fail to connect to http://localhost:1/?code=SOMECODE&scope=..."
        $stderr.puts "Copy the value of SOMECODE from the address and paste it below"

        code = $stdin.readline.chomp
        $stderr.puts "-----------------------------------------------"
        credentials = authorizer.get_and_store_credentials_from_code(
            user_id: user_id, code: code)
    end
    credentials
end                                                                                                                                      ```

Solution 3:[3]

A solution for python.

As google_auth_oauthlib shows, InstalledAppFlow.run_console has been deprecated after Feb 28, 2022. And if you are using google-ads-python, you can just replace flow.run_console() by flow.run_local_server().

Solution 4:[4]

Let me post the "proper" solution as a separate answer, which is to actually follow the recommended procedure by implementing an HTTP listener in the ruby app. If this is running on an offline machine the listener will never get the code, but you can still paste the code from the failing URL.


require 'colorize'
require 'sinatra/base'

def run_local_server(authorizer, port, user_id)

    require 'thin'
    Thin::Logging.silent = true

    Thread.new {

        Thread.current[:server] = Sinatra.new do

            enable :quiet
            disable :logging
            set :port, port
            set :server, %w[ thin ]

            get "/" do
                request = Rack::Request.new env
                state = {
                    code:  request["code"],
                    error: request["error"],
                    scope: request["scope"]
                }
                raise Signet::AuthorizationError, ("Authorization error: %s" % [ state[:error] ] ) if state[:error]
                raise Signet::AuthorizationError, "Authorization code missing from the request" if state[:code].nil?
                credentials = authorizer.get_and_store_credentials_from_code(

                    user_id: user_id,
                    code: state[:code],
                    scope: state[:scope],
                )
                [
                    200,
                    { "Content-Type" => "text/plain" },
                    "All seems to be OK. You can close this window and press ENTER in the application to proceed.",
                ]
            end

        end
        Thread.current[:server].run!
    }

end

client_id = Google::Auth::ClientId.new(ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'])
token_store = Google::Auth::Stores::FileTokenStore.new(:file => ENV['GOOGLE_CREDENTIAL_STORE'])
PORT = 6969
redirect_uri = "http://localhost:#{PORT}/"
authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store, redirect_uri)
credentials = authorizer.get_credentials(user_id)
if credentials.nil? then
  server_thread = run_local_server(authorizer, PORT, user_id)
  url = authorizer.get_authorization_url
  $stderr.puts ""
  $stderr.puts "-----------------------------------------------"
  $stderr.puts "Requesting authorization for '#{user_id.yellow}'"
  $stderr.puts "Open the following URL in your browser and authorize the application."
  $stderr.puts
  $stderr.puts url.yellow.bold
  $stderr.puts
  $stderr.puts "?? If you are authorizing on a different machine, you will have to port-forward"
  $stderr.puts "so your browser can reach #{redirect_uri.yellow}"
  $stderr.puts
  $stderr.puts "?? If you get a " << "This site can't be reached".red << " error in the browser,"
  $stderr.puts "just copy the code which is in the code= part of the failing address on the next line."
  $stderr.puts "E.g., you need only the " << "green".bold.green << " part of the address which looks like"
  $stderr.puts "#{redirect_uri}?code=".yellow << "4/QMoyZIyzt8uXO6j...j8ajEEjfd".bold.green << "&scope=email%20profile...".yellow
  $stderr.puts "-----------------------------------------------"
  code = $stdin.readline.chomp
  server_thread[:server].stop!
  server_thread.join
  credentials = authorizer.get_credentials(user_id)
  # If the redirect failed, the user must have provided us with a code on their own
  if credentials.nil? then
    credentials = authorizer.get_and_store_credentials_from_code(user_id: user_id, code: code, scope: scope)
  end
end

In short, we run a web server expecting the redirect from the browser. It takes the code the browser sent, or it takes the code pasted by the user.

Solution 5:[5]

I've fixed this problem with recreate my App in google console. And I think the problem was with redirect_url. I had this problem when I was using 'Android' type of App in google console (in this case you can't configure redirect url). In my android App I'm using google auth with WebView so the best option here use use 'Web' type for your app in google console.

Solution 6:[6]

"Hello world" for this error:

Generating an authentication URL

https://github.com/googleapis/google-api-nodejs-client#generating-an-authentication-url

const {google} = require('googleapis');

const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// generate a url that asks permissions for Blogger and Google Calendar scopes
const scopes = [
  'https://www.googleapis.com/auth/blogger',
  'https://www.googleapis.com/auth/calendar'
];

const url = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',

  // If you only need one scope you can pass it as a string
  scope: scopes
});

If something goes wrong the first step is to Re Check again the three values of the google.auth.OAuth2 function.

1 of 2

Compare to the store values under Google APIs console:

  1. YOUR_CLIENT_ID
  2. YOUR_CLIENT_SECRET enter image description here
  3. YOUR_REDIRECT_URL - enter image description here For example http://localhost:3000/login

enter image description here

2 of 2 (environment variables)

A lot of times the values store inside .env. So re-check the env and the output under your files - for example index.ts (Even use console.log).

.env

# Google Sign-In (OAuth)
G_CLIENT_ID=some_id_1234
G_CLIENT_SECRET=some_secret_1234
PUBLIC_URL=http://localhost:3000

index

const auth = new google.auth.OAuth2(
  process.env.G_CLIENT_ID,
  process.env.G_CLIENT_SECRET,
  `${process.env.PUBLIC_URL}/login`
);

SUM:

Something like this will not work

const oauth2Client = new google.auth.OAuth2(
  "no_such_id",
  "no_such_secret",
  "http://localhost:3000/i_forgot_to_Authorised_this_url"
);

Solution 7:[7]

For headless Python scripts that need sensitive scopes, continuing to use run_console now produces the following (and the flow likely fails):

DeprecationWarning: New clients will be unable to use `InstalledAppFlow.run_console` starting on Feb 28, 2022. All clients will be unable to use this method starting on Oct 3, 2022. Use `InstalledAppFlow.run_local_server` instead. For details on the OOB flow deprecation, see https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html?m=1#disallowed-oob

The official solution is to migrate to a flow that spins up a local server to handle the OAuth redirect, but this will not work on remote headless systems.

The solution Google adopted in gcloud is to run a local server on the same machine as the user's browser and then have the user copy the redirect URL requested from this local server back to the remote machine. Note that this requires having gcloud installed both on the remote machine and on the user's workstation.

As a hack for situations where installing a script to echo back the redirect URL on the workstation is not practical, we can use a redirect URL that is guaranteed to fail and just have the user copy back the URL of the error page on which they will land after authorization is complete.

import urllib
from google_auth_oauthlib.flow import InstalledAppFlow

def run_console_hack(flow):
    flow.redirect_uri = 'http://localhost:1'
    auth_url, _ = flow.authorization_url()
    print(
        "Visit the following URL:",
        auth_url,
        "After granting permissions, you will be redirected to an error page",
        "Copy the URL of that error page",
        sep="\n"
    )
    redir_url = input("URL: ")
    query = urllib.parse.urlparse(redir_url).query
    code = urllib.parse.parse_qs(query)['code'][0]
    flow.fetch_token(code=code)
    return flow.credentials

scopes = ['https://www.googleapis.com/auth/drive.file']
flow = InstalledAppFlow.from_client_secrets_file(secrets_file, scopes)
credentials = run_console_hack(flow)

We could also ask the user to pass back the code query string parameter directly but that is likely to be confusing and error-prone.

The use of 1 as the port number means that the request is guaranteed to fail, rather than potentially hit some service that happens to be running on that port. (e.g. Chrome will fail with ERR_UNSAFE_PORT without even trying to connect)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DaImTo
Solution 2
Solution 3 Mingpeng Wang
Solution 4
Solution 5 Djek-Grif
Solution 6
Solution 7