Google OAuth 2.0 失败并出现错误 400:invalid_request 对于某些 client_id,但对于同一项目中的其他人效果很好

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

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

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

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

这是围绕授权流程的代码部分,不同客户端 ID 的 URL 是在 $stderr.puts url 行生成的。它与 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
                                                                                                                                          

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

  • GenerateAccessToken GenerateAuthorizationCode
  • GenerateAccessTokenImplicitGrant
  • 刷新访问令牌

Google Oauth Policy

我向 Google OAuth 团队的某个人发送了一封电子邮件。这是他们回复的要点。

我担心你的问题与 Making Google OAuth interactions safer by using more secure OAuth flows

有关

google 的当前建议是按照此处的建议移动到使用 localhost/loopback 重定向:instructions-oob 或者如果您使用 non-sensitive,则使用 OAuth for devices flow范围,需要一个无头的解决方案。

这是针对这种情况的一个糟糕的解决方法:

将问题中发布的代码中的 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                                                                                                                                      ```

让我 post“正确的”解决方案作为一个单独的答案,即通过在 ruby 应用程序中实施 HTTP 侦听器来实际遵循推荐的程序。如果这是 运行ning 在离线机器上,侦听器将永远不会获得代码,但您仍然可以从失败的 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

简而言之,我们 运行 一个网络服务器期待来自浏览器的重定向。它采用浏览器发送的代码,或者采用用户粘贴的代码。

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

python 的解决方案。

作为google_auth_oauthlib shows, InstalledAppFlow.run_console has been deprecated after Feb 28, 2022. And if you are using google-ads-python,您可以将flow.run_console()替换为flow.run_local_server()

"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 个,共 2 个

Google APIs console 下的存储值比较:

  1. YOUR_CLIENT_ID
  2. YOUR_CLIENT_SECRET
  3. YOUR_REDIRECT_URL - 例如 http://localhost:3000/login

2 个中的 2 个(环境变量)

很多时候值存储在 .env 中。所以 re-check env 和文件下的输出 - 例如 index.ts(甚至使用 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`
);

总和:

这样的东西是行不通的

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

对于需要敏感范围的无头 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 重定向,但这在远程无头系统上不起作用。

gcloud中采用的解决方案Google是运行与用户浏览器在同一台机器上的本地服务器,然后让用户复制重定向URL从该本地服务器请求返回到远程机器。请注意,这需要在远程计算机和用户工作站上都​​安装 gcloud

对于在工作站上安装脚本回显重定向 URL 不可行的情况,我们可以使用保证会失败的重定向 URL用户复制回错误页面的URL,他们将在授权完成后登陆。

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)

我们也可以要求用户直接传回 code 查询字符串参数,但这可能会造成混淆 error-prone.

使用 1 作为端口号意味着请求肯定会失败,而不是可能命中恰好在该端口上 运行ning 的某些服务。 (例如,Chrome 将在 ERR_UNSAFE_PORT 甚至不尝试连接的情况下失败)