48

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
                                                                                                                                          
chutz
  • 2,256
  • 2
  • 25
  • 38
  • is your account a standard google account or a workspace account? You can only set to internal if its workspace. Can you double check that they are all desktop clients. – Linda Lawton - DaImTo Mar 02 '22 at 08:03
  • Yes, this is a workspace account. The "December 2021" link in my answer is a real link generated by my app, and anyone can verify the `org_internal` error (which means that it works well). Yes, I am looking at the client IDs right now and they are all "Desktop". For extra confirmation: The January 2021 client id is `566372898811-b89vfj17vaqpcd7bat5tdvbdcpbb41li.apps.googleusercontent.com`, it is Desktop, and gives the invalid_request error; December 2021 client id is `566372898811-htvr8md883tk25e1t8q7kabacpmh94dc.apps.googleusercontent.com`, it is Desktop and works. – chutz Mar 02 '22 at 09:58
  • 1
    Please edit your question and include your code. I will ping someone at google – Linda Lawton - DaImTo Mar 02 '22 at 10:33
  • I wonder if its related to this [manual-copypaste](https://developers.google.com/identity/protocols/oauth2/native-app#manual-copypaste) – Linda Lawton - DaImTo Mar 02 '22 at 10:37
  • @DaImTo The code is not really interesting, but I am adding it to the question. I see that there is some deprecation going on, which would mean that running such command line tools inside WSL or a headless Linux box may become... quite inconvenient. – chutz Mar 02 '22 at 11:05
  • Im going to email someone that might be able to clear this up. This will not be optimal for my curl scripts either. – Linda Lawton - DaImTo Mar 02 '22 at 11:19

9 Answers9

23

Please see https://stackoverflow.com/a/71491500/1213346 for a "proper" solution. This answer is just an ugly workaround that the community seems to like.

...

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                                                                                                                                      ```
chutz
  • 2,256
  • 2
  • 25
  • 38
  • Hi, what language is that? Do you have a similar snippet for Python? – Dariusz Dudziński Mar 05 '22 at 20:46
  • 1
    @DariuszDudziński this Ruby, but the only meaningful change is replacing `urn:ietf:wg:oauth:2.0:oob` with `http://localhost:1/`. Also changed the comments but these are cosmetic. If you have code that worked before, you can try changing the redirect URL to what I mentioned. – chutz Mar 07 '22 at 07:57
  • can confirm this works. It sucks but if you need it for an internal quick project it is fine. If anyone is using ruby its : `authorizer.get_authorization_url(base_url: "http://localhost:1/" )` – An Le Mar 15 '22 at 23:12
  • 1
    FYI for others: Note that you can't just change `redirect_uri` in the URL that was provided by the authenticator. You have to set `http://localhost:1/` before making the request! (We tried to just change redirect_uri in the url that had contained `urn:ietf:wg:oauth:2.0:oob` and Google didn't accept the "code". I hacked my local copy of `googleads-php-lib-git/examples/Auth/GetRefreshToken.php` and set `const REDIRECT_URI = 'https://localhost:1';`, re-ran GetRefreshToken.php and it worked.) – KJ7LNW Apr 15 '22 at 20:09
  • I tried in Google Colab as follows: `credentials = flow.run_local_server(host="localhost", port=1,` and it didn't worked for me. – Marco Aurelio Fernandez Reyes Jul 01 '22 at 22:19
13

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.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
  • Thanks for digging this out. Too bad that I cannot use the device authentication flow as that seems to only support profile and drive scopes. Mighty nice of Google to not break the already existing applications :) – chutz Mar 03 '22 at 00:37
  • 2
    OMG, `gcloud auth login` is not working anymore either. ‍♂️ – chutz Mar 03 '22 at 04:59
  • It also means to make it work as Google recommends : OAuth 2.0 Client IDs credentials you create to access application must be of type Web Application. Cause only then you can configure Authorised redirect URIs for that credential. You also must use that redirect URI you defined for any call to Google APIs. To make authentication seamless for desktop apps you need to run own server on that redirect URI as well to get the code sent by Google. – Constantin Zagorsky Apr 16 '22 at 20:48
  • you can still use installed app the default redirect uri is http ://127.0.0.1 – Linda Lawton - DaImTo Apr 16 '22 at 21:00
  • If you follow official documentation nothing will work for native apps described there: https://developers.google.com/identity/protocols/oauth2/native-app#request-parameter-redirect_uri . This answer pretty much updated official Google documentation. Thank you – Constantin Zagorsky Apr 16 '22 at 21:07
  • I bet next step for Google is to oblige everyone to use SMS messages to get credentials for APIs calls :) Hope it never happens. Anyways I still must approve first call made with credentials on my phone which doesn't stop me from using API but it can I suppose block further calls if I answer that it wasn't me who made that API access call. – Constantin Zagorsky Apr 16 '22 at 21:16
  • Just now tested AuthorizationCodeInstalledApp class in Java and it works pretty well but I had to still explicitly configure host and port for LocalServerReceiver, as well as allow it in Allowed redirect URIs in https://console.cloud.google.com/apis/credentials. The last element is to use FileDataStoreFactory to persist credentials so that redirect URI can be skipped for subsequent APIs calls. – Constantin Zagorsky Apr 16 '22 at 22:40
  • All of the client libraries should have bene updated prior to this change. if you have any issues make sure to post them over on the github issue forum for that library. – Linda Lawton - DaImTo Apr 17 '22 at 16:45
  • @DaImTo I guess Google then need to update an authorization docs for Google Ads API because they still refer the older `code` flow that results in the same error as described https://developers.google.com/google-ads/api/docs/client-libs/python/oauth-desktop – Дмитро Булах May 16 '22 at 10:28
  • its a process its going to take time for them to update all the samples. – Linda Lawton - DaImTo May 16 '22 at 11:20
12

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().

  • Indeed, this was my inspiration for the "proper" solution in https://stackoverflow.com/a/71491500/1213346 – chutz Mar 24 '22 at 11:18
  • 1
    I actually got it to work in colab by starting a server in python. `run_local_server()` still tries `localhost` but I was able to hack the URL at the end to point to the server running in my notebook. – Fuhrmanator Apr 18 '22 at 20:07
  • 1
    @Fuhrmanator can you elaborate, please? - I'm trying using OAuth 2.0 in Google Colab, but, I'm unable to get it working. This is [my question](https://stackoverflow.com/q/72805453/12511801) describing my problem I'm currently facing in Google Colab and OAuth 2.0. – Marco Aurelio Fernandez Reyes Jul 01 '22 at 22:22
7

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'

# A simplistic local server to receive authorization tokens from the browser
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

# Returns user credentials for the given scope. Requests authorization
# if requrired.
def user_credentials_for(scope, user_id = 'default')
        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 failing URL below. Copy the whole thing, starting with #{redirect_uri.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
            begin
                require 'uri'
                require 'cgi'
                code = CGI.parse(URI.parse(code).query)['code'][0]
            rescue StandardException
                # Noop, if we could not get a code out of the URL, maybe it was
                # not the URL but the actual code.
            end

            credentials = authorizer.get_and_store_credentials_from_code(
                user_id: user_id,
                code: code,
                scope: scope,
            )
        end
    end
    credentials
end

credentials = user_credentials_for(['https://www.googleapis.com/auth/drive.readonly'])

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.

chutz
  • 2,256
  • 2
  • 25
  • 38
2

"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"
);
Ezra Siton
  • 6,887
  • 2
  • 25
  • 37
2

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.

from urllib.parse import parse_qs, urlparse
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 (http://localhost:1/?state=...)",
        sep="\n"
    )
    redir_url = input("URL: ")
    code = parse_qs(urlparse(redir_url).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)

Grisha Levit
  • 8,194
  • 2
  • 38
  • 53
  • This is yet another cringy workaround which I would definitely not want to encourage when there are "proper" answers already: https://stackoverflow.com/a/71491500/1213346 for Ruby (since original question is about Ruby) and https://stackoverflow.com/a/71524639/1213346 for Python. P.S. Very creative that you parse the code from the URL. – chutz May 02 '22 at 05:05
  • I agree it's a crappy way to do it. But the local server approach requires that your browser redirect you to a server running on the same machine as the script, which is not possible in many scenarios. – Grisha Levit May 04 '22 at 15:52
  • This is exactly the situation I am in however, unfortunately this solution does not seem to work for me as of 07/30/22. – Cade M Jul 30 '22 at 21:53
  • @CadeM what's not working about it for you? Note that you are expected to hit an error page after authorizing the request in your browser. – Grisha Levit Aug 01 '22 at 23:27
  • 1
    @GrishaLevit I cannot edit my original comment, but it was not working due to an error on my part. My issue was that I had `http://localhost` and not `https://localhost` in my authorized redirect URIs. – Cade M Aug 02 '22 at 00:59
0

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.

Djek-Grif
  • 1,391
  • 18
  • 18
0

In my case, had to update plugins. by running following command-

bundle exec fastlane update_plugins

With this redirect uri was getting created properly as

https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&include_granted_scopes=true&redirect_uri=http://localhost:8081&response_type=code&scope=https://www.googleapis.com/auth/cloud-platform&state=2ce8a59b2d403f3a89fa635402bfc5c4
-4

steps.oauth.v2.invalid_request 400 This error name is used for multiple different kinds of errors, typically for missing or incorrect parameters sent in the request. If is set to false, use fault variables (described below) to retrieve details about the error, such as the fault name and cause.

  • GenerateAccessToken GenerateAuthorizationCode
  • GenerateAccessTokenImplicitGrant
  • RefreshAccessToken

Google Oauth Policy

hehe
  • 1
  • Welcome to stack please read [how-to-answer](https://stackoverflow.com/help/how-to-answer) Im not sure how you think that answer would help it doesnt even address the fact that the author is using an internal app or that its desktop. Not to mention your link is for Google cloud auth and not google apis auth. – Linda Lawton - DaImTo Mar 02 '22 at 08:06
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 02 '22 at 17:15