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;
我正在尝试按照概述的步骤 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;