具有服务器到服务器身份验证的 Google OAuth2 返回“invalid_grant”

Posted

技术标签:

【中文标题】具有服务器到服务器身份验证的 Google OAuth2 返回“invalid_grant”【英文标题】:Google OAuth2 with Server to Server authentication returns "invalid_grant" 【发布时间】:2015-03-23 01:34:04 【问题描述】:

我正在尝试按照here 概述的步骤来获取访问令牌,以便与具有 OAuth2 的 Google 日历 API 一起使用。在尝试汇总并签署 jwt 后,我​​总是以 400“错误请求”响应结束,并出现错误“invalid_grant”。

我非常严格地遵循这些步骤,并多次仔细检查每一行。我还详尽地浏览了我能找到的关于该主题的每一篇文章。在多年在线寻找解决方案之后,我非常困惑,以至于我写了我的第一个 SO 问题。

我已经尝试过的常见解决方案:

1) 我的系统时钟与 ntp 时间同步

2) 我使用的是 iss 的电子邮件,而不是客户端 ID。

3) 我的发行时间和到期时间都是 UTC

4) 我确实查看了 access_type=offline 参数,但它似乎不适用于这种服务器到服务器的场景。

5) 我没有指定prn参数。

6) 各种其他杂项

我知道有 Google 库可以帮助管理此问题,但我有理由说明为什么我需要通过自己签署 jwt 而不使用提供的库来使其正常工作。此外,到目前为止,我看到的许多问题和示例似乎都在使用 accounts.google.com/o/oauth2/auth 作为基本 url,而我上面链接的文档似乎指定请求转到 www。 googleapis.com/oauth2/v3/token 代替(因此似乎许多现有问题可能适用于不同的场景)。无论如何,我完全被难住了,不知道还能尝试什么。这是我的 C# 代码,其中包含一些特定的字符串。

    public static string GetBase64UrlEncoded(byte[] input)
    
        string value = Convert.ToBase64String(input);
        value = value.Replace("=", string.Empty).Replace('+', '-').Replace('/', '_');
        return value;
    

    static void Main(string[] args)
                           
        DateTime baseTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        DateTime now = DateTime.Now.ToUniversalTime();
        int ticksIat = ((int)now.Subtract(baseTime).TotalSeconds);
        int ticksExp = ((int)now.AddMinutes(55).Subtract(baseTime).TotalSeconds);
        string jwtHeader = @"""typ"":""JWT"", ""alg"":""RS256""";
        string jwtClaimSet = string.Format(@"""iss"":""************-********************************@developer.gserviceaccount.com""," +
                                           @"""scope"":""https://www.googleapis.com/auth/calendar.readonly""," +
                                           @"""aud"":""https://www.googleapis.com/oauth2/v3/token"",""exp"":0,""iat"":1", ticksExp, ticksIat);                        
        byte[] headerBytes = Encoding.UTF8.GetBytes(jwtHeader);
        string base64jwtHeader = GetBase64UrlEncoded(headerBytes);
        byte[] claimSetBytes = Encoding.UTF8.GetBytes(jwtClaimSet);
        string base64jwtClaimSet = GetBase64UrlEncoded(claimSetBytes);            
        string signingInputString = base64jwtHeader + "." + base64jwtClaimSet;
        byte[] signingInputBytes = Encoding.UTF8.GetBytes(signingInputString);
        X509Certificate2 pkCert = new X509Certificate2("<path to cert>.p12", "notasecret");                                                           
        RSACryptoServiceProvider  rsa = (RSACryptoServiceProvider)pkCert.PrivateKey;
        CspParameters cspParam = new CspParameters
        
            KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        ;

        RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider(cspParam)  PersistKeyInCsp = false ;                
        byte[] signatureBytes = cryptoServiceProvider.SignData(signingInputBytes, "SHA256");
        string signatureString = GetBase64UrlEncoded(signatureBytes);            
        string finalJwt = signingInputString + "." + signatureString;

        HttpClient client = new HttpClient();
        string url = "https://www.googleapis.com/oauth2/v3/token?grant_type=urn%3aietf%3aparams%3aoauth%3agrant-type%3ajwt-bearer&assertion=" + finalJwt;
        HttpResponseMessage message = client.PostAsync(url, new StringContent(string.Empty)).Result;
        string result = message.Content.ReadAsStringAsync().Result;
    

这是使用我在我的谷歌账户上设置的谷歌“服务账户”,生成的私钥及其对应的 .p12 文件直接使用。

有人用这种方法来工作吗?我将非常感谢任何帮助!

【问题讨论】:

【参考方案1】:

您正在 POST 到令牌端点,但参数是作为查询字符串的一部分发送的。您应该将参数作为 POST 正文中的 URL 表单编码值发送。例如:

var params = new List<KeyValuePair<string, string>>();
params.Add(new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"));
params.Add(new KeyValuePair<string, string>("assertion", finalJwt));
var content = new FormUrlEncodedContent(pairs);
var message = client.PostAsync(url, content).Result; 

【讨论】:

感谢您的快速回复!这确实解决了这个问题。错过了多么尴尬的事情......【参考方案2】:

试试这个来获取访问令牌-

            String serviceAccountEmail = "xxxxxxx.gserviceaccount.com";

            String keyFilePath = System.Web.HttpContext.Current.Server.MapPath("~/Content/Security/file.p12"); ////.p12 file location

            if (!File.Exists(keyFilePath))
            
                Console.WriteLine("An Error occurred - Key file does not exist");
                return null;
            

            string[] scopes = new string[] 
                CloudVideoIntelligenceService.Scope.CloudPlatform,  ///CloudVideoIntelligence scope
            YouTubeService.Scope.YoutubeForceSsl,                   ///Youtube scope
            TranslateService.Scope.CloudTranslation                 ///Translation scope
            ;
            var certificate = new X509Certificate2(keyFilePath, "notasecret", X509KeyStorageFlags.Exportable);
            ServiceAccountCredential credential = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer(serviceAccountEmail)
                
                    Scopes = scopes
                .FromCertificate(certificate));


            var token = Google.Apis.Auth.OAuth2.GoogleCredential.FromServiceAccountCredential(credential).UnderlyingCredential.GetAccessTokenForRequestAsync().Result;//retrive token

            return token;

【讨论】:

以上是关于具有服务器到服务器身份验证的 Google OAuth2 返回“invalid_grant”的主要内容,如果未能解决你的问题,请参考以下文章

来自服务器的 Google 身份验证

Google Cloud Run 身份验证服务到服务

服务器端应用程序的 Google+ 登录身份验证不起作用

PHP Google 服务帐户身份验证日历 API

使用 Json 文件进行 Google 服务帐户身份验证

使用后端服务器进行 Google 身份验证所需的范围