为什么在 HomeGraph API return 403 Forbidden 上请求同步?
Why does Request Sync on HomeGraph API return 403 Forbidden?
问题
当我在 Google HomeGraph API 上调用 "Request Sync" 时,我收到“403 Forbidden”响应。
背景
我正在编写智能家居操作,并已成功实现 SYNC、QUERY 和 EXECUTE。在我的手机上测试我可以看到并与设备交互。我现在正在尝试实施 Request Sync,但似乎无法与 API 交互。我正在发出似乎成功的访问令牌请求。令牌总是以 "ya29.c." 开头,在我天真的理解中,这表明一个空的 header 和有效负载(在 https://jwt.io). However, when testing it at https://accounts.google.com/o/oauth2/tokeninfo?access_token= 上尝试它似乎有效,同时显示我的服务帐户唯一 ID 和我想要的范围. 当我调用 API 时,无论是手动 posting 数据,还是通过 Google 自己的代码,它都会给我一个直接的 403 错误。我不知道在哪里除了异常 objects 之外,我可以获得有关此错误的更多信息。我是 GCP 的新手,找不到任何类型的日志。鉴于我尝试了不同的方法和所有 return 403 我倾向于怀疑问题更多的是帐户或 credential-related 而不是代码,但不能确定。
API键
(我无法再重现与 API 密钥丢失或无效相关的任何错误)。
虽然文档没有显示,但我看到有人使用 API 密钥。当我没有在 p12 证书中包含 API 密钥,或者包含不正确的密钥时,它会出错(缺少 API 密钥,或者相应地 API 密钥无效)。我在 IAM 中创建了一个不受限制的 API 密钥,并且正在使用它。我似乎无法明确地将其与 HomeGraph API 联系起来,但它表示它可以调用任何 API。
代码
此示例获取访问令牌,然后尝试通过 POST 使用和不使用 API 密钥调用 API。然后它尝试通过 Google 库代码验证并调用 API。每个都失败并返回 403。
using Google;
using Google.Apis.Auth.OAuth2;
using Google.Apis.HomeGraphService.v1;
using Google.Apis.HomeGraphService.v1.Data;
using Google.Apis.Services;
using Lambda.Core.Constants;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using static Google.Apis.HomeGraphService.v1.DevicesResource;
public class Example
{
public void RequestSync()
{
const string UrlWithoutKey = @"https://homegraph.googleapis.com/v1/devices:requestSync";
const string UrlWithKey = @"https://homegraph.googleapis.com/v1/devices:requestSync?key=" + OAuthConstants.GoogleApiKey;
string accessToken = this.GetAccessToken();
// Manual Attempt 1
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithoutKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// Manual Attempt 2
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// SDK Attempt
try
{
this.CallRequestSyncApiWithSdk();
}
catch (GoogleApiException ex)
{
// Google.Apis.Requests.RequestError
// The caller does not have permission[403]
// Errors[Message[The caller does not have permission] Location[- ] Reason[forbidden] Domain[global]]
// at Google.Apis.Requests.ClientServiceRequest`1.ParseResponse(HttpResponseMessage response) in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 243
// at Google.Apis.Requests.ClientServiceRequest`1.Execute() in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 167
string msg = ex.Message;
}
}
private string GetAccessToken()
{
string defaultScope = "https://www.googleapis.com/auth/homegraph";
string serviceAccount = OAuthConstants.GoogleServiceAccountEmail; // "??????@??????.iam.gserviceaccount.com"
string certificateFile = OAuthConstants.CertificateFileName; // "??????.p12"
var oAuth2 = new GoogleOAuth2(defaultScope, serviceAccount, certificateFile); // As per
bool status = oAuth2.RequestAccessTokenAsync().Result;
// This access token at a glance appears invalid due to an empty header and payload,
// But verifies ok when tested here: https://accounts.google.com/o/oauth2/tokeninfo?access_token=
return oAuth2.AccessToken;
}
private string CallRequestSyncApiManually(string accessToken, string url)
{
string apiRequestBody = @"{""agentUserId"": """ + OAuthConstants.TestAgentUserId + @"""}";
var client = new HttpClient();
var request = (HttpWebRequest)WebRequest.Create(url);
var data = Encoding.ASCII.GetBytes(apiRequestBody);
request.Method = "POST";
request.Accept = "application/json";
request.ContentType = "application/json";
request.ContentLength = data.Length;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
return responseString;
}
private void CallRequestSyncApiWithSdk()
{
var certificate = new X509Certificate2(OAuthConstants.CertificateFileName, OAuthConstants.CertSecret, X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(OAuthConstants.GoogleServiceAccountEmail)
{
Scopes = new[] { "https://www.googleapis.com/auth/homegraph" },
}.FromCertificate(certificate));
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
// Complains if API key is not provided, even though we're using a certificate from a Service Account
ApiKey = OAuthConstants.GoogleApiKey,
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
}
账户配置
账户截图。 (我还不允许 post 图片,所以它们是链接)
HomeGraph is enabled
My API Key is unrestricted
My Service Account has Owner & Service Account Token Creator enabled
更新
我已尝试按照 Devunwired 的建议跳过手动获取访问令牌。虽然这确实消除了我因未提供 API 密钥而导致的错误,但我仍然以 403 结束。我手动执行访问令牌部分的推理是调试我使用 [= 获得的 403 的一部分60=] 呼叫。这样我至少可以看到部分过程在起作用。我很高兴使用库版本作为解决方案,因为访问令牌似乎不是问题所在。
public void GoogleLibraryJsonCredentialExample()
{
try
{
GoogleCredential credential;
using (var stream = new FileStream(OAuthConstants.JsonCredentialsFileName, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(new[] { OAuthConstants.GoogleScope });
}
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
catch (Exception ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
}
担忧
我是否需要从经过验证的域或 white-listed 域进行 API 调用?目前,我 运行 它来自我开发机器上的控制台应用程序 运行。我对域验证的理解是它不适用于来电,因此应该不是问题。
I am making what seems to be successful requests for an Access Token.
使用 Google 客户端库时,您不需要手动请求 OAuth 访问令牌。他们通常使用您从 GCP 控制台提供的凭据在内部处理此过程。
Although the documentation doesn't show it, I've seen some people use an API key. Indeed, it is mandatory to include it for the SDK approach.
我们不建议使用 API 键方法访问主页图 API。您应该使用服务帐户凭据。 API 密钥在技术上适用于 Request Sync 方法,但您将无法使用 [=27] 验证 Report State =]键。
您在尝试构建没有 API 密钥的 HomeGraphServiceService
时收到错误消息可能表明您使用的凭据设置不正确(没有私钥或可能缺少作用域)。提供服务帐户凭据的推荐方法是以 JSON 格式而不是证书下载它们,从 JSON 生成凭据的代码应如下所示:
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(scopes);
}
您可以在 authentication guide.
中找到用于验证 API 的其他 C# 示例
问题与我允许与 HomeGraph API 或该用户交谈无关。相反,它是 HomeGraph 想要调用我的智能家居操作的地方,但访问令牌已过期。尝试刷新令牌时,我的错误实现导致了直接的 403,然后 Google 转发回给我。
对于那些感兴趣的人,问题是我没有省略永不过期的令牌的到期日期,而是将其设置为 DateTime.MaxValue
(随后通过一些进一步处理发送)。不幸的是,当它最终被转换为一个 int 时,它是一个超过 int.Max
的值。随后的到期时间设置为纪元(即过去),因此令牌验证因到期而失败。
对于仍然遇到同样问题的其他人,请仔细检查您的 agentUserId
是否与您的 SYNC 输出负载中显示的值完全匹配。就我而言,我已经检查过了。
非常感谢所有看过这篇文章的人。
问题
当我在 Google HomeGraph API 上调用 "Request Sync" 时,我收到“403 Forbidden”响应。
背景
我正在编写智能家居操作,并已成功实现 SYNC、QUERY 和 EXECUTE。在我的手机上测试我可以看到并与设备交互。我现在正在尝试实施 Request Sync,但似乎无法与 API 交互。我正在发出似乎成功的访问令牌请求。令牌总是以 "ya29.c." 开头,在我天真的理解中,这表明一个空的 header 和有效负载(在 https://jwt.io). However, when testing it at https://accounts.google.com/o/oauth2/tokeninfo?access_token= 上尝试它似乎有效,同时显示我的服务帐户唯一 ID 和我想要的范围. 当我调用 API 时,无论是手动 posting 数据,还是通过 Google 自己的代码,它都会给我一个直接的 403 错误。我不知道在哪里除了异常 objects 之外,我可以获得有关此错误的更多信息。我是 GCP 的新手,找不到任何类型的日志。鉴于我尝试了不同的方法和所有 return 403 我倾向于怀疑问题更多的是帐户或 credential-related 而不是代码,但不能确定。
API键
(我无法再重现与 API 密钥丢失或无效相关的任何错误)。
虽然文档没有显示,但我看到有人使用 API 密钥。当我没有在 p12 证书中包含 API 密钥,或者包含不正确的密钥时,它会出错(缺少 API 密钥,或者相应地 API 密钥无效)。我在 IAM 中创建了一个不受限制的 API 密钥,并且正在使用它。我似乎无法明确地将其与 HomeGraph API 联系起来,但它表示它可以调用任何 API。
代码
此示例获取访问令牌,然后尝试通过 POST 使用和不使用 API 密钥调用 API。然后它尝试通过 Google 库代码验证并调用 API。每个都失败并返回 403。
using Google;
using Google.Apis.Auth.OAuth2;
using Google.Apis.HomeGraphService.v1;
using Google.Apis.HomeGraphService.v1.Data;
using Google.Apis.Services;
using Lambda.Core.Constants;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using static Google.Apis.HomeGraphService.v1.DevicesResource;
public class Example
{
public void RequestSync()
{
const string UrlWithoutKey = @"https://homegraph.googleapis.com/v1/devices:requestSync";
const string UrlWithKey = @"https://homegraph.googleapis.com/v1/devices:requestSync?key=" + OAuthConstants.GoogleApiKey;
string accessToken = this.GetAccessToken();
// Manual Attempt 1
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithoutKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// Manual Attempt 2
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// SDK Attempt
try
{
this.CallRequestSyncApiWithSdk();
}
catch (GoogleApiException ex)
{
// Google.Apis.Requests.RequestError
// The caller does not have permission[403]
// Errors[Message[The caller does not have permission] Location[- ] Reason[forbidden] Domain[global]]
// at Google.Apis.Requests.ClientServiceRequest`1.ParseResponse(HttpResponseMessage response) in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 243
// at Google.Apis.Requests.ClientServiceRequest`1.Execute() in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 167
string msg = ex.Message;
}
}
private string GetAccessToken()
{
string defaultScope = "https://www.googleapis.com/auth/homegraph";
string serviceAccount = OAuthConstants.GoogleServiceAccountEmail; // "??????@??????.iam.gserviceaccount.com"
string certificateFile = OAuthConstants.CertificateFileName; // "??????.p12"
var oAuth2 = new GoogleOAuth2(defaultScope, serviceAccount, certificateFile); // As per
bool status = oAuth2.RequestAccessTokenAsync().Result;
// This access token at a glance appears invalid due to an empty header and payload,
// But verifies ok when tested here: https://accounts.google.com/o/oauth2/tokeninfo?access_token=
return oAuth2.AccessToken;
}
private string CallRequestSyncApiManually(string accessToken, string url)
{
string apiRequestBody = @"{""agentUserId"": """ + OAuthConstants.TestAgentUserId + @"""}";
var client = new HttpClient();
var request = (HttpWebRequest)WebRequest.Create(url);
var data = Encoding.ASCII.GetBytes(apiRequestBody);
request.Method = "POST";
request.Accept = "application/json";
request.ContentType = "application/json";
request.ContentLength = data.Length;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
return responseString;
}
private void CallRequestSyncApiWithSdk()
{
var certificate = new X509Certificate2(OAuthConstants.CertificateFileName, OAuthConstants.CertSecret, X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(OAuthConstants.GoogleServiceAccountEmail)
{
Scopes = new[] { "https://www.googleapis.com/auth/homegraph" },
}.FromCertificate(certificate));
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
// Complains if API key is not provided, even though we're using a certificate from a Service Account
ApiKey = OAuthConstants.GoogleApiKey,
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
}
账户配置
账户截图。 (我还不允许 post 图片,所以它们是链接)
HomeGraph is enabled
My API Key is unrestricted
My Service Account has Owner & Service Account Token Creator enabled
更新
我已尝试按照 Devunwired 的建议跳过手动获取访问令牌。虽然这确实消除了我因未提供 API 密钥而导致的错误,但我仍然以 403 结束。我手动执行访问令牌部分的推理是调试我使用 [= 获得的 403 的一部分60=] 呼叫。这样我至少可以看到部分过程在起作用。我很高兴使用库版本作为解决方案,因为访问令牌似乎不是问题所在。
public void GoogleLibraryJsonCredentialExample()
{
try
{
GoogleCredential credential;
using (var stream = new FileStream(OAuthConstants.JsonCredentialsFileName, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(new[] { OAuthConstants.GoogleScope });
}
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
catch (Exception ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
}
担忧
我是否需要从经过验证的域或 white-listed 域进行 API 调用?目前,我 运行 它来自我开发机器上的控制台应用程序 运行。我对域验证的理解是它不适用于来电,因此应该不是问题。
I am making what seems to be successful requests for an Access Token.
使用 Google 客户端库时,您不需要手动请求 OAuth 访问令牌。他们通常使用您从 GCP 控制台提供的凭据在内部处理此过程。
Although the documentation doesn't show it, I've seen some people use an API key. Indeed, it is mandatory to include it for the SDK approach.
我们不建议使用 API 键方法访问主页图 API。您应该使用服务帐户凭据。 API 密钥在技术上适用于 Request Sync 方法,但您将无法使用 [=27] 验证 Report State =]键。
您在尝试构建没有 API 密钥的 HomeGraphServiceService
时收到错误消息可能表明您使用的凭据设置不正确(没有私钥或可能缺少作用域)。提供服务帐户凭据的推荐方法是以 JSON 格式而不是证书下载它们,从 JSON 生成凭据的代码应如下所示:
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(scopes);
}
您可以在 authentication guide.
中找到用于验证 API 的其他 C# 示例问题与我允许与 HomeGraph API 或该用户交谈无关。相反,它是 HomeGraph 想要调用我的智能家居操作的地方,但访问令牌已过期。尝试刷新令牌时,我的错误实现导致了直接的 403,然后 Google 转发回给我。
对于那些感兴趣的人,问题是我没有省略永不过期的令牌的到期日期,而是将其设置为 DateTime.MaxValue
(随后通过一些进一步处理发送)。不幸的是,当它最终被转换为一个 int 时,它是一个超过 int.Max
的值。随后的到期时间设置为纪元(即过去),因此令牌验证因到期而失败。
对于仍然遇到同样问题的其他人,请仔细检查您的 agentUserId
是否与您的 SYNC 输出负载中显示的值完全匹配。就我而言,我已经检查过了。
非常感谢所有看过这篇文章的人。