Google 登录:使用 google-auth Python 包时“未找到密钥 ID xxxx 的证书”

zzh*_*eng 2 google-authentication jwt google-signin google-auth-library

我正在维护一个网站及其移动应用程序(iOS 和 Android)。对于移动应用程序中的 Google 登录,我在服务器端使用google-auth Python 包。

从大约一个月前开始,我开始从服务器端收到与Google登录相关的错误报告。错误消息如下所示:

未找到密钥 ID 728f4016652079b9ed99861bb09bafc5a45baa86 的证书。

服务器端 Google Sign-In 身份验证后端遵循此文档

from google.oauth2 import id_token
from google.auth.transport import requests

# ...

try:
    # The following line may raise ValueError with message:
    # Certificate for key id xxxx not found.
    id_info = id_token.verify_oauth2_token(google_id_token, requests.Request())

    if id_info['aud'] not in VALID_CLIENT_IDS:
        logger.error('Invalid aud from Google ID token: %s', id_info['aud'])
        raise ValueError('Unverified audience.')
    # ...
except ValueError as exc:
    logger.error('Fail to verify Google ID token: %s', exc, extra={'request': request})
Run Code Online (Sandbox Code Playgroud)

该错误来自google.auth.jwt 模块当根据 Google 公共证书列表验证 Google 颁发的 JWT 时,

深入研究 google-auth 代码,我可以看到该verify_oauth2_token()函数正在从 URL https://www.googleapis.com/oauth2/v1/certs获取 Google 公共证书。有时,对于从某些 Android 手机发送的某些 Google ID 令牌,似乎无法在该 URL 中找到密钥 ID。

以下是一些可能有用的其他详细信息:

  • iOS应用似乎没有这种问题。从USER_AGENT标题中,我可以看到该错误仅发生在 Android 应用程序 ( USER_AGENT=okhttp/3.11.0) 中。而且这种情况仅发生在部分 Android 设备上,并非全部。
  • 我想知道这种情况是否只发生在中国的 Android 手机上(例如,如果它们通过 VPN 连接)。所以我也检查了用户的IP地址。但事实证明,其中一些用户来自欧洲。
  • 某些密钥 ID 在服务器错误日志中一次又一次重复出现。例如,aa436c3f63b281ce0d976da0b51a34860ff960eb从 11 月初到现在(12 月底),密钥 id 出现了数十次。
  • 我经常看到这个错误,每天好几次(10~30次)。

我的网站运行在以下环境:

  • 操作系统:Linux(CentOS 7)64位
  • Apache 2 与 mod_wsgi 4.5.24
  • Python 3.6.7 和 Django 2.1.2
  • google-auth 版本:尝试了 1.3.0 和 1.6.1

由于我无法用我的 iPhone 或 Android 手机(华为 P20,在法国购买)重现此问题,所以我完全陷入困境。

但是,我的一个朋友现在遇到了这个问题,他从香港购买了 Android 手机。这让我想到,对于某些国家/地区,Google 登录是否可能使用除https://www.googleapis.com/oauth2/v1/certs上的公共证书之外的其他证书?

我不认为这是 google-auth 包中的错误。我想知道你们中是否有人听说过这个错误,并且可以给我一些关于其可能原因的提示?

提前致谢!

zzh*_*eng 5

好吧,我终于想通了。我在这里发布我的发现,希望对其他人有帮助。

服务器端Python代码没有任何问题。失败的原因是客户端应用程序提交了过期的 Google ID 令牌。

这是我的 LoginActivity 的固定版本:

...

@OnClick(R.id.google_sign_in_button)
void loginWithGoogle() {
    //
    // If user has already signed in to our app with Google, sign him out first.
    //
    // NOTE: This step is required, or the ID token might not pass the server-side validation!
    //
    // After sign-in, we need to get the user's ID token issued and signed by Google, and send
    // it back to our server for validation.
    //
    // Google is rotating its OAuth2 certificate regularly, so an old ID token issued long time
    // ago by Google might not pass the server-side validation -- if the certificate used to
    // sign the ID token has expired.
    //
    // This may happen when user has already signed in to our app with Google. In such case,
    // the ID token we get from the user's Google account is obsolete. Our server will fail to
    // validate it, with the error message:
    //
    //     Fail to verify Google ID token: Certificate for key id xxx not found.
    //
    // Google recommends using the `silentSignIn` method for the already-signed-in user
    // (see step 2 of: https://developers.google.com/identity/sign-in/android/backend-auth).
    // For the sake of simplicity, we don't do that. Instead, we go directly to step 3
    // by signing user out, giving him the option to sign-in again.
    //
    final GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
    if (account != null) {
        // User has already signed in: Sign out and sign in again.
        // NOTE: THIS IS THE FIX TO MY PROBLEM.
        mGoogleSignInClient.signOut().addOnCompleteListener(this, new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                startGoogleSignInActivity();
            }
        });
    } else {
        // User is not yet signed in: Start the Google sign-in flow.
        startGoogleSignInActivity();
    }
}

private void startGoogleSignInActivity() {
    final Intent intent = mGoogleSignInClient.getSignInIntent();
    startActivityForResult(intent, REQUEST_LOGIN_WITH_GOOGLE);
}
Run Code Online (Sandbox Code Playgroud)

关键点是:我需要检查用户是否已经使用 Google 登录。如果是,我需要注销用户并重新启动“使用 Google 登录”活动。

由于 Android 对 Google 帐户有本机支持,我想如果用户已经通过身份验证(在某些其他应用程序或系统范围内),操作系统可能会缓存用户的 Google 帐户。但该缓存帐户可能包含过期的 ID 令牌。强制用户注销并重新登录将为我提供一个全新的 ID 令牌。

这也解释了为什么我的iOS应用程序没有这个问题。因为iOS从不缓存用户的Google帐户。