谷歌OAuth 2.0对于某些客户端的client_id出现错误400:invalid_request,但对于同一项目中的其他客户端有效。

52
我们有一些应用程序(或者也许我们应该称它们为一些脚本),使用Google APIs来简化一些管理任务。最近,在同一个项目中制作另一个client_id之后,我开始收到一个类似于localhost redirect_uri does not work for Google Oauth2 (results in 400: invalid_request error)的错误消息。即:

错误400:无效请求

您不能登录此应用程序,因为它不符合Google OAuth 2.0政策,以保持应用程序的安全性。

您可以告诉应用程序开发人员,此应用程序不符合一个或多个Google验证规则。

请求详细信息:

本节内容由应用程序开发人员提供。此内容尚未经过Google审核或验证。

如果您是应用程序开发人员,请确保这些请求详细信息符合Google政策。

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

我如何解决这个错误?需要注意的是:

  • 该项目的OAuth同意屏幕被标记为“内部”。因此,任何提到Google对该项目的审查或发布状态的内容都是无关紧要的。
  • 我已经启用了域名下的“信任内部应用程序”选项。
  • 在同一项目中,另一个客户端ID可以正常工作,并且两个客户端ID之间没有明显区别——它们都是“桌面”类型,只给我提供了不同的客户端ID和客户端密钥。
  • 这是一个命令行脚本,因此我使用“复制/粘贴”验证方法在此处记录,因此使用urn:ietf:wg:oauth:2.0:oob重定向URI(复制/粘贴是在没有浏览器的无头机器上运行此脚本的唯一友好方式)。
  • 我能够在开发域中复现相同的问题。我有三个客户端ID。最旧的一个来自2021年1月,另一个来自2021年12月,而我今天创建的一个则来自2022年3月。其中,仅2021年12月的那个可以正常工作,并且在接受或拒绝“Error 403:org_internal”之前让我选择要进行身份验证的帐户(这是预期的)。其他两个则会给我一个“Error 400:invalid_request”,甚至不让我选择“internal”帐户。以下是我的应用程序生成的URL(我使用ruby google客户端API),它们之间唯一的区别是客户端ID——2021年1月, 2021年12月, 2022年3月

这是关于授权流程的代码部分,不同客户端ID的URL是在$stderr.puts url行上生成的。它与此处的官方示例文档 (截至本文撰写时的版本)基本相同。


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
                                                                                                                                          

您的账户是标准的 Google 账户还是 Workspace 账户?如果是 Workspace 账户,您只能将其设置为内部。请再次确认它们都是桌面客户端。 - Linda Lawton - DaImTo
是的,这是一个工作区账户。我的回答中的“2021年12月”链接是由我的应用程序生成的真实链接,任何人都可以验证org_internal错误(这意味着它运行良好)。是的,我现在正在查看客户端ID,它们全部都是“桌面”。为了进一步确认:2021年1月的客户端ID是566372898811-b89vfj17vaqpcd7bat5tdvbdcpbb41li.apps.googleusercontent.com,它是桌面版,并显示无效请求错误;2021年12月的客户端ID是566372898811-htvr8md883tk25e1t8q7kabacpmh94dc.apps.googleusercontent.com,它是桌面版并且有效。 - chutz
1
请编辑您的问题并包含您的代码。我会联系谷歌的某个人。 - Linda Lawton - DaImTo
我想知道这是否与此手动复制粘贴有关。 - Linda Lawton - DaImTo
@DaImTo 这段代码并不是很有趣,但我会将它添加到问题中。我发现有一些过时的情况正在发生,这意味着在WSL或无头Linux框中运行此类命令行工具可能会变得相当不方便。 - chutz
我将给某个人发送电子邮件,他可能能够澄清这个问题。这对我的curl脚本也不是最佳选择。 - Linda Lawton - DaImTo
9个回答

23
请查看https://dev59.com/D1EG5IYBdhLWcg3wLV2N#71491500,以获得“正确”的解决方案。本答案只是社区似乎喜欢的丑陋的解决方法。
...
以下是此情况的一种令人不适的解决方法:
在问题中发布的代码中,将urn:ietf:wg:oauth:2.0:oob替换为http://localhost:1/。这使流程可以顺利进行,我的浏览器会被重定向并失败,并且我会收到错误信息。
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

现在将失败URL中的code值复制,粘贴到应用程序中,然后就大功告成了...和之前一样 :)

P.S. 这是更新的“工作”版本:


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                                                                                                                                      ```

你好,那是什么编程语言?你有Python的类似代码片段吗? - Dariusz Dudziński
1
@DariuszDudziński 这是 Ruby 代码,但唯一有意义的更改是将 urn:ietf:wg:oauth:2.0:oob 替换为 http://localhost:1/。还更改了注释,但这些只是表面上的。如果您之前有可用的代码,可以尝试将重定向 URL 更改为我提到的内容。 - chutz
可以确认这个有效。虽然不太好,但如果您需要它用于内部快速项目,那就没问题。如果有人在使用Ruby,可以使用以下代码: authorizer.get_authorization_url(base_url: "http://localhost:1/" ) - An Le
1
提醒其他人:请注意,您不能仅更改认证器提供的URL中的“redirect_uri”。在发出请求之前,您必须设置“http://localhost:1/”!(我们尝试仅更改包含“urn:ietf:wg:oauth:2.0:oob”的URL中的redirect_uri,但Google不接受“code”。我修改了本地副本的“googleads-php-lib-git / examples / Auth / GetRefreshToken.php”,并设置了“const REDIRECT_URI ='https://localhost:1';”,重新运行GetRefreshToken.php,然后它就可以工作了。) - KJ7LNW
我在Google Colab中尝试了以下代码:credentials = flow.run_local_server(host="localhost", port=1,但是它对我没有起作用。 - Marco Aurelio Fernandez Reyes

13

感谢您挖掘出这个问题。很遗憾我不能使用设备认证流程,因为它似乎只支持配置文件和驱动器范围。谷歌真是太好了,没有破坏已经存在的应用程序 :) - chutz
2
天啊,gcloud auth login也不再起作用了。 ‍♂️ - chutz
我敢打赌,谷歌的下一步是强制每个人使用短信消息来获取API调用的凭据 :) 希望这永远不会发生。无论如何,我仍然必须在我的手机上批准使用凭据进行的第一个调用,这并不妨碍我使用API,但如果我回答说不是我进行了该API访问调用,它可能会阻止进一步的调用。 - Constantin Zagorsky
@DaImTo 我猜Google需要更新Google Ads API的授权文档,因为他们仍然参考旧的code流程,这会导致与描述相同的错误。https://developers.google.com/google-ads/api/docs/client-libs/python/oauth-desktop - Дмитро Булах
这是一个过程,他们更新所有示例需要时间。 - Linda Lawton - DaImTo
显示剩余5条评论

12

Python的解决方案。

正如google_auth_oauthlib显示的那样,InstalledAppFlow.run_console 已经在2022年2月28日之后被弃用。如果您正在使用google-ads-python,只需用 flow.run_local_server() 替换 flow.run_console() 即可。


确实,这是我在 https://dev59.com/D1EG5IYBdhLWcg3wLV2N#71491500 中提供“正确”解决方案的灵感来源。 - chutz
1
我实际上通过在Python中启动服务器,在colab中使其工作了。 run_local_server() 仍然尝试使用 localhost,但是我能够在末尾黑客URL以指向在我的笔记本电脑上运行的服务器。 - Fuhrmanator
1
@Fuhrmanator,您能详细说明一下吗?- 我正在尝试在Google Colab中使用OAuth 2.0,但是我无法使其正常工作。这是我的问题,描述了我目前在Google Colab和OAuth 2.0中遇到的问题。 - Marco Aurelio Fernandez Reyes

8

让我发布一个“合适”的解决方案作为单独的答案,这是通过在Ruby应用程序中实现HTTP监听器来实际遵循建议的步骤。如果此应用程序在离线机器上运行,监听器将永远不会获取代码,但您仍然可以从失败的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'])

简而言之,我们运行一个 Web 服务器,期望浏览器的重定向。它接收浏览器发送的代码,或用户复制粘贴的代码。

用户.id的“黄色”方法是什么? - Ziv Kesten
1
@ZivKesten Colorize库对字符串类进行了猴子补丁。它添加了颜色方法,可以在终端上更改其颜色。 - Gareve

3
对于需要敏感范围的无界面Python脚本,继续使用run_console现在会产生以下结果(并且流程可能失败):
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

官方的解决方案是迁移到一个启动本地服务器来处理OAuth重定向的流程,但这在远程无头系统上无法工作。
Google在gcloud中采用的解决方案是在与用户浏览器相同的机器上运行一个本地服务器,然后让用户将从该本地服务器请求的重定向URL复制回远程机器。请注意,这需要在远程机器和用户的工作站上都安装gcloud。
对于在工作站上安装脚本以回显重定向URL不可行的情况,我们可以使用一个被保证会失败的重定向URL,然后让用户将授权完成后他们将落地的错误页面的URL复制回来,作为一种替代方法。
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)

我们也可以要求用户直接传回code查询字符串参数,但这可能会令人困惑且容易出错。

1用作端口号意味着该请求必定失败,而不是有可能连接到运行在该端口上的某个服务。(例如,Chrome将以ERR_UNSAFE_PORT错误失败,甚至都不尝试连接)


这是另一种令人不快的解决方法,当已经有“正确”的答案时,我绝对不想鼓励它:https://dev59.com/D1EG5IYBdhLWcg3wLV2N#71491500 适用于 Ruby(因为原始问题是关于 Ruby 的),以及 https://dev59.com/D1EG5IYBdhLWcg3wLV2N#71524639 适用于 Python。附言:非常有创意,您从 URL 解析代码。 - chutz
我同意这是一种糟糕的方法。但是本地服务器的方法要求你的浏览器将你重定向到运行在与脚本相同机器上的服务器,这在许多情况下都是不可能的。 - Grisha Levit
这正是我所处的情况,然而很遗憾,这个解决方案在2022年7月30日似乎对我不起作用。 - Cade M
@CadeM,你遇到了什么问题?请注意,在浏览器授权请求后,您应该会看到一个错误页面。 - Grisha Levit
1
@GrishaLevit 我无法编辑我的原始评论,但这是由于我的错误导致它无法正常工作。我的问题在于我在我的授权重定向URI中使用了 http://localhost 而非 https://localhost - Cade M

2

"Hello world" 是这个错误的提示信息:

生成身份验证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
});

如果出现问题,第一步是再次检查google.auth.OAuth2函数的三个值。
1 of 2
Google APIs控制台中存储的值进行比较:
  1. YOUR_CLIENT_ID
  2. YOUR_CLIENT_SECRET enter image description here
  3. YOUR_REDIRECT_URL - enter image description here 例如http://localhost:3000/login
enter image description here 2 of 2(环境变量)
很多时候,值存储在.env中。因此,请重新检查env以及文件输出下面的内容-例如index.ts (甚至使用console.log)。
# Google Sign-In (OAuth)
G_CLIENT_ID=some_id_1234
G_CLIENT_SECRET=some_secret_1234
PUBLIC_URL=http://localhost:3000

索引

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

SUM:

类似这样的东西是行不通的。

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

0
在我的情况下,必须通过运行以下命令来更新插件 -
bundle exec fastlane update_plugins

使用此重定向URI,将正确创建

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

0
我已经通过在Google控制台中重新创建我的应用程序来解决了这个问题。我认为问题出在redirect_url上。当我在Google控制台中使用“Android”类型的应用程序时,我遇到了这个问题(在这种情况下,您无法配置重定向URL)。在我的Android应用程序中,我正在使用WebView进行Google身份验证,因此在Google控制台中,最好选择“Web”类型来使用您的应用程序。

-4

steps.oauth.v2.invalid_request 400 此错误名称用于多种不同类型的错误,通常用于请求中缺少或不正确的参数。如果设置为 false,请使用故障变量(如下所述)检索有关错误的详细信息,例如故障名称和原因。

  • GenerateAccessToken 生成访问令牌
  • GenerateAuthorizationCode 生成授权码
  • GenerateAccessTokenImplicitGrant 生成隐式授权访问令牌
  • RefreshAccessToken 刷新访问令牌

Google Oauth Policy {{link1:Google Oauth政策}}


欢迎来到 Stack,请阅读 如何回答。我不确定您认为那个答案如何有帮助,它甚至没有解决作者正在使用内部应用程序或其桌面版本的事实。更不用说您提供的链接是关于 Google 云身份验证而不是 Google API 身份验证的了。 - Linda Lawton - DaImTo
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接