Почему запрос синхронизации в HomeGraph API возвращает 403 Forbidden?

Проблема

Когда я вызываю «Запрос на синхронизацию» в API Google HomeGraph, я получаю ответ «403 Forbidden».

Фон

Я пишу Smart Home Action и успешно внедрил SYNC, QUERY и EXECUTE. Тестирование на моем мобильном устройстве я могу видеть и взаимодействовать с устройствами в порядке. Сейчас я пытаюсь реализовать Request Sync, но не могу взаимодействовать с API. Я делаю то, что кажется успешным запросом токена доступа. Маркер всегда начинается с «ya29.c». что в моем наивном понимании предполагает пустой заголовок и полезную нагрузку (пробуем на https://jwt.io). Однако при тестировании на https://accounts.google.com/o/oauth2/tokeninfo?access_token= он кажется действительным, показывая как уникальный идентификатор моей учетной записи службы, так и предполагаемую область действия. Когда я обращаюсь к API, либо вручную публикуя данные, либо через собственный код Google, я получаю грубую ошибку 403. Я не знаю, где я могу получить больше информации об этой ошибке, кроме объектов исключений. Я новичок в GCP и не смог найти никакого журнала. Учитывая, что я пробовал разные методы, и все они возвращают 403, я склонен подозревать, что проблема больше связана с учетной записью или учетными данными, чем с кодом, но не могу быть уверенным.

API-ключ

(Я больше не могу воспроизводить какие-либо ошибки, связанные с отсутствием или недействительностью ключей API).

Хотя документация не показывает этого, я видел, как некоторые люди используют ключ API. Когда я не включаю ключ API с сертификатом p12 или включаю неверный, возникают ошибки (либо с отсутствующим ключом API, либо с недействительным ключом API соответственно). Я создал неограниченный ключ API в IAM и использую его. Я не могу явно связать это с HomeGraph API, но там сказано, что он может вызывать любой API.

Код

Этот пример извлекает токен доступа, а затем пытается вызвать API через POST с ключом API и без него. Затем он пытается пройти аутентификацию и вызвать API через код библиотеки Google. Каждый терпит неудачу с 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 https://stackoverflow.com/questions/26478694/how-to-produce-jwt-with-google-oauth2-compatible-algorithm-rsa-sha-256-using-sys
        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();
    }
}

Конфигурация учетной записи

Скриншоты аккаунта. (мне еще не разрешено публиковать изображения, так что это ссылки)

HomeGraph включен

У моего ключа API нет ограничений

В моей служебной учетной записи включен создатель токена владельца и служебной учетной записи

Обновления

Я попытался пропустить ручное получение токена доступа в соответствии с предложением Devunwired. Хотя это устраняет ошибку, которую я получал из-за того, что не предоставлял ключ API, я все равно получаю 403. Мое объяснение того, что часть токена доступа выполняется вручную, было частью отладки 403, которое я получал с вызовом API. Таким образом, я мог, по крайней мере, увидеть, как работает часть процесса. Я рад использовать версию библиотеки для решения, поскольку токен доступа не является проблемой.

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;
    }
}

Обеспокоенность

Возможно ли, что мне нужно выполнять вызов API из проверенного домена или домена из белого списка? На данный момент я запускаю его из консольного приложения, работающего на моем компьютере для разработки. Я понимаю проверку домена так, что она не применяется к входящим вызовам и, следовательно, не должна быть проблемой.


person Adam Hill    schedule 31.01.2020    source источник
comment
Можете ли вы предоставить более подробную информацию об ошибке, которую вы получаете, когда пропускаете ключ API из инициализатора службы?   -  person devunwired    schedule 31.01.2020
comment
Моя ошибка - я только что снова прочитал ошибку, и в ней больше не упоминается ключ API. В какой-то момент за последние несколько дней я получил сообщение об ошибке, связанное с отсутствующим ключом API и недействительным ключом API на разных этапах. В текущей версии, как указано выше, эта ошибка представляет собой просто 403. Я обновлю вопрос и устраню двусмысленность вокруг ключа API.   -  person Adam Hill    schedule 31.01.2020


Ответы (2)


Я делаю то, что кажется успешным запросом токена доступа.

Вам не нужно вручную запрашивать токены доступа OAuth при использовании клиентских библиотек Google. Обычно они обрабатывают этот процесс внутри компании, используя учетные данные, которые вы предоставляете из консоли GCP.

Хотя в документации это не показано, я видел, как некоторые люди используют ключ API. Действительно, его обязательно включать для подхода SDK.

Мы не рекомендуем использовать ключ API для доступа к Home Graph API. Вы должны использовать учетные данные сервисной учетной записи. Ключи API технически подходят для метода Запросить синхронизацию, но вы не сможете аутентифицировать Состояние отчета с помощью ключа API.

Тот факт, что вы получаете сообщение об ошибке при попытке создать HomeGraphServiceService без ключа API, может свидетельствовать о том, что используемые вами учетные данные настроены неправильно (отсутствует закрытый ключ или, возможно, отсутствующие области действия). Рекомендуемый способ предоставления учетных данных служебной учетной записи — загрузить их в формате JSON, а не сертификат, а код для создания учетных данных из JSON должен выглядеть примерно так:

GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
    credential = GoogleCredential.FromStream(stream).CreateScoped(scopes);
}

Дополнительные примеры C# для аутентификации API можно найти в руководстве по аутентификации.

person devunwired    schedule 31.01.2020
comment
Большое спасибо за ваш ответ. Я обновил вопрос с ошибкой ключа API. Хотя этот подход (с использованием учетных данных JSON) устраняет ошибку ключа API, я все равно получаю 403. Мне интересно, правильно ли мой код, использующий библиотеку, для создания запроса на синхронизацию запроса, поскольку я не видел примеров .NET. этого конкретного запроса, и просто написал то, что казалось разумным на основе intellisense, т. е. создать объект RequestSyncRequest и вызвать его выполнение. - person Adam Hill; 31.01.2020

Проблема не была связана с моим разрешением общаться с HomeGraph API или с этим пользователем. Вместо этого HomeGraph хотел вызвать мое действие «Умный дом», но срок действия токена доступа истек. При попытке обновить токен ошибочная реализация с моей стороны привела к тупой ошибке 403, которую затем мне переслал Google.

Для тех, кто заинтересован, проблема заключалась в том, что вместо того, чтобы пропустить дату истечения срока действия токена, который никогда не должен истечь, я установил его на DateTime.MaxValue (впоследствии отправленный через некоторую дальнейшую обработку). К сожалению, когда это, наконец, приводится к типу int, это значение превышает int.Max. Последующее время истечения срока действия было установлено на эпоху (т. е. в прошлом), поэтому проверка токена завершилась неудачно из-за истечения срока действия.

Для тех, у кого еще есть такая же проблема, дважды проверьте, что agentUserId точно соответствует значению, показанному в полезной нагрузке вывода SYNC. В моем случае я проверил это.

Большое спасибо всем, кто посмотрел это.

person Adam Hill    schedule 04.02.2020