【问题标题】:Validate Google Id Token验证 Google ID 令牌
【发布时间】:2016-12-27 22:18:09
【问题描述】:

我正在使用 ASP.NET Core 为 Android 客户端提供 API。 Android 以 Google 帐户的身份登录,并将 JWT(ID 令牌)作为不记名令牌传递给 API。我的应用程序正在运行,它确实通过了身份验证检查,但我认为它没有验证令牌签名。

根据 Google 的文档,我可以调用此 url 来执行此操作:https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123,但我在服务器端找不到合适的挂钩来执行此操作。同样根据谷歌文档,我可以以某种方式使用客户端访问 API 来完成它,而无需每次都调用服务器。

我的配置代码:

app.UseJwtBearerAuthentication( new JwtBearerOptions()
{

    Authority = "https://accounts.google.com",
    Audience = "hiddenfromyou.apps.googleusercontent.com",
    TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateAudience = true,
        ValidIssuer = "accounts.google.com"
    },
    RequireHttpsMetadata = false,
    AutomaticAuthenticate = true,
    AutomaticChallenge = false,
});

如何获取 JWTBearer 中间件来验证签名?我快要放弃使用 MS 中间件并自己动手了。

【问题讨论】:

    标签: c# google-api asp.net-core google-oauth jwt


    【解决方案1】:

    有几个不同的ways,您可以在其中验证服务器端 ID 令牌的完整性:

    1. “手动”——不断下载谷歌的公钥,验证签名,然后是每一个字段,包括iss;我在这里看到的主要优势(尽管在我看来很小)是您可以最大限度地减少发送给 Google 的请求数量。
    2. “自动” - 在 Google 的端点上执行 GET 以验证此令牌 https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}
    3. 使用 Google API 客户端库 - 例如 official one

    这是第二个的样子:

    private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}";
    
    public ProviderUserDetails GetUserDetails(string providerToken)
    {
        var httpClient = new MonitoredHttpClient();
        var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken));
    
        HttpResponseMessage httpResponseMessage;
        try
        {
            httpResponseMessage = httpClient.GetAsync(requestUri).Result;
        }
        catch (Exception ex)
        {
            return null;
        }
    
        if (httpResponseMessage.StatusCode != HttpStatusCode.OK)
        {
            return null;
        }
    
        var response = httpResponseMessage.Content.ReadAsStringAsync().Result;
        var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response);
    
        if (!SupportedClientsIds.Contains(googleApiTokenInfo.aud))
        {
            Log.WarnFormat("Google API Token Info aud field ({0}) not containing the required client id", googleApiTokenInfo.aud);
            return null;
        }
    
        return new ProviderUserDetails
        {
            Email = googleApiTokenInfo.email,
            FirstName = googleApiTokenInfo.given_name,
            LastName = googleApiTokenInfo.family_name,
            Locale = googleApiTokenInfo.locale,
            Name = googleApiTokenInfo.name,
            ProviderUserId = googleApiTokenInfo.sub
        };
    }
    

    GoogleApiTokenInfo 类:

    public class GoogleApiTokenInfo
    {
    /// <summary>
    /// The Issuer Identifier for the Issuer of the response. Always https://accounts.google.com or accounts.google.com for Google ID tokens.
    /// </summary>
    public string iss { get; set; }
    
    /// <summary>
    /// Access token hash. Provides validation that the access token is tied to the identity token. If the ID token is issued with an access token in the server flow, this is always
    /// included. This can be used as an alternate mechanism to protect against cross-site request forgery attacks, but if you follow Step 1 and Step 3 it is not necessary to verify the 
    /// access token.
    /// </summary>
    public string at_hash { get; set; }
    
    /// <summary>
    /// Identifies the audience that this ID token is intended for. It must be one of the OAuth 2.0 client IDs of your application.
    /// </summary>
    public string aud { get; set; }
    
    /// <summary>
    /// An identifier for the user, unique among all Google accounts and never reused. A Google account can have multiple emails at different points in time, but the sub value is never
    /// changed. Use sub within your application as the unique-identifier key for the user.
    /// </summary>
    public string sub { get; set; }
    
    /// <summary>
    /// True if the user's e-mail address has been verified; otherwise false.
    /// </summary>
    public string email_verified { get; set; }
    
    /// <summary>
    /// The client_id of the authorized presenter. This claim is only needed when the party requesting the ID token is not the same as the audience of the ID token. This may be the
    /// case at Google for hybrid apps where a web application and Android app have a different client_id but share the same project.
    /// </summary>
    public string azp { get; set; }
    
    /// <summary>
    /// The user's email address. This may not be unique and is not suitable for use as a primary key. Provided only if your scope included the string "email".
    /// </summary>
    public string email { get; set; }
    
    /// <summary>
    /// The time the ID token was issued, represented in Unix time (integer seconds).
    /// </summary>
    public string iat { get; set; }
    
    /// <summary>
    /// The time the ID token expires, represented in Unix time (integer seconds).
    /// </summary>
    public string exp { get; set; }
    
    /// <summary>
    /// The user's full name, in a displayable form. Might be provided when:
    /// The request scope included the string "profile"
    /// The ID token is returned from a token refresh
    /// When name claims are present, you can use them to update your app's user records. Note that this claim is never guaranteed to be present.
    /// </summary>
    public string name { get; set; }
    
    /// <summary>
    /// The URL of the user's profile picture. Might be provided when:
    /// The request scope included the string "profile"
    /// The ID token is returned from a token refresh
    /// When picture claims are present, you can use them to update your app's user records. Note that this claim is never guaranteed to be present.
    /// </summary>
    public string picture { get; set; }
    
    public string given_name { get; set; }
    
    public string family_name { get; set; }
    
    public string locale { get; set; }
    
    public string alg { get; set; }
    
    public string kid { get; set; }
    }
    

    【讨论】:

    • 哇,自 2012 年以来,我一直是 Google .Net 官方客户端库的贡献者。您确定 C# 还没有吗? Google 也不建议向 tokeninfo 发送垃圾邮件,您应该在本地验证 Token_id。 github.com/google/google-api-dotnet-client
    • 道歉@DalmTo,你是对的!我已经编辑了我的答案
    • GoogleApiTokenInfo 是在哪里定义的?这是您在 Google SDK 中创建或定义的自定义类吗?
    • @BobBlack - 我已经更新了我的答案以包含它;它是根据 Google 的规范创建的
    • 对于那些构建 Chrome 扩展程序或 Chrome 应用程序的人,chrome.identity.getAuthToken() 方法只提供了一个access_token。幸运的是,@AlexandruMarculescu 在选项 2 中建议的端点也支持验证这种类型的令牌:googleapis.com/oauth2/v3/tokeninfo?access_token={0}
    【解决方案2】:

    根据这个 github issue,您现在可以使用 GoogleJsonWebSignature.ValidateAsync 方法来验证 Google 签名的 JWT。只需将idToken 字符串传递给方法即可。

    var validPayload = await GoogleJsonWebSignature.ValidateAsync(idToken);
    Assert.IsNotNull(validPayload);
    

    如果不是有效令牌,则返回null

    注意要使用这种方法,你需要先安装Google.Apis.Authnuget。

    【讨论】:

    • 根据文档,当令牌无效时,这不会返回 null。它会抛出 InvalidJwtException,因此您需要使用 try catch,而不是断言或检查 null。
    【解决方案3】:

    Google 在openId connect 的文档中声明

    出于调试目的,您可以使用 Google 的 tokeninfo 端点。假设您的 ID 令牌的值为 XYZ123。

    您不应该使用该端点来验证您的 JWT。

    验证 ID 令牌需要几个步骤:

    1. 验证 ID 令牌是否由发行者正确签名。 Google 颁发的令牌使用在 discovery document 的 jwks_uri 字段中指定的 URI 中找到的证书之一进行签名。
    2. 验证 ID 令牌中 iss 的值是否等于 https://accounts.google.com 或 accounts.google.com。
    3. 验证 ID 令牌中的 aud 值是否等于您应用的客户端 ID。
    4. 验证 ID 令牌的到期时间 (exp) 尚未过去。
    5. 如果您在请求中传递了 hd 参数,请验证 ID 令牌具有与您的 G Suite 托管域匹配的 hd 声明。

    有一个关于如何验证它们的官方示例项目here。不幸的是,我们还没有将它添加到 Google .Net 客户端库中。它已被记录为issue

    【讨论】:

    • takeinfo 端点可用于验证 JWT,它检查 issexp 是否具有预期值,并检查签名以验证它。 Sign in with google
    【解决方案4】:

    我认为值得一提的是,您可以使用@Alexandru Marculescu 答案进行身份验证,但文档中有一条说明不要使用方法 2。

    Per the documentation under Calling the tokeninfo endpoint(已更改为https://oauth2.googleapis.com/tokeninfo?id_token=XYZ123

    It is not suitable for use in production code as requests may be throttled or otherwise subject to intermittent errors.
    

    验证 idToken 的推荐方法是调用 Google API 客户端库。这就是我执行验证检查的方式

    using Google.Apis.Auth;
    
    ...
    
    public async Task<GoogleJsonWebSignature.Payload> ValidateIdTokenAndGetUserInfo(string idToken)
    {
      if (string.IsNullOrWhiteSpace(idToken))
      {
        return null;
      }
    
      try
      {
        return await GoogleJsonWebSignature.ValidateAsync(idToken);
      }
      catch (Exception exception)
      {
        _Logger.LogError(exception, $"Error calling ValidateIdToken in GoogleAuthenticateHttpClient");
        return null;
      }
    }
    

    【讨论】:

      【解决方案5】:

      所以,我发现 OpenIDConnect 规范有一个 /.well-known/ url,其中包含验证令牌所需的信息。这包括访问签名的公钥。 JWT 中间件从权威机构形成 .well-known url,检索信息,然后自行验证它。

      这个问题的简短回答是验证已经在中间件中进行,没有什么可做的。

      【讨论】:

        【解决方案6】:
             private const string GoogleApiTokenInfoUrl = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={0}";
        
               Public ProviderUserDetails ValidateGoogleToken(string providerToken)        
               {
        
                var httpClient = new HttpClient();
        
                var requestUri = new Uri(string.Format(GoogleApiTokenInfoUrl, providerToken));
        
                HttpResponseMessage httpResponseMessage;
                try
                {
                    httpResponseMessage = httpClient.GetAsync(requestUri).Result;
                }
                catch (Exception ex)
                {
                    return null;
                }
        
                if (httpResponseMessage.StatusCode != HttpStatusCode.OK)
                {
                    return null;
                }
        
                var response = httpResponseMessage.Content.ReadAsStringAsync().Result;
                var googleApiTokenInfo = JsonConvert.DeserializeObject<GoogleApiTokenInfo>(response);
        
                return new ProviderUserDetails
                {
                    Email = googleApiTokenInfo.email,
                    FirstName = googleApiTokenInfo.given_name,
                    LastName = googleApiTokenInfo.family_name,
                    Locale = googleApiTokenInfo.locale,
                    Name = googleApiTokenInfo.name,
                    ProviderUserId = googleApiTokenInfo.sub
                };
            }
        

        【讨论】:

        • 虽然此代码可能会回答问题,但提供有关此代码为何和/或如何回答问题的额外上下文可提高其长期价值。
        • 实际上我关心的是发布这个答案以澄清用户可以验证前端开发人员提供的用于谷歌登录的令牌,或者使用此代码用户可以验证并从令牌中获取用户详细信息。通过使用它,他们可以使用 jwt 生成自己的令牌。
        猜你喜欢
        • 1970-01-01
        • 2020-07-25
        • 2015-06-27
        • 1970-01-01
        • 1970-01-01
        • 2021-07-19
        • 2016-08-11
        • 2017-01-24
        相关资源
        最近更新 更多