Google 具有服务器到服务器身份验证的 OAuth2 returns "invalid_grant"

Google OAuth2 with Server to Server authentication returns "invalid_grant"

我正在尝试按照概述的步骤 here 获取访问令牌,以便与带有 OAuth2 的 Google 日历 API 一起使用。在尝试组合并签署 jwt 之后,我总是以 400 "Bad request" 响应结束,错误 "invalid_grant"。

我已经非常仔细地按照步骤操作,并且仔细检查了每一行多次。我还详尽地查看了关于我能找到的主题的每一个 post。在网上寻找解决方案多年后,我完全被难住了,以至于我写了我的第一个 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;
    }

这是使用我在 google 帐户上设置的 google "Service Account",生成的私钥及其对应的 .p12 文件直接使用。

有人用过这种方法吗?我非常感谢任何帮助!

您正在 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; 

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

            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;