DialogFlow/Actions:允许 Google 助理用户通过 Actions App 在 Google 日历中创建事件

DialogFlow/Actions: Allow Google Assistant user to create Event in Google Calendar from Actions App

Target/Summary: 我有一个在 Google DialogFlow 中开发的 Actions 应用程序,我希望用户能够使用该应用程序(来自 Google 助手)创建 Google 日历事件。换句话说,对用户进行身份验证以允许 my 应用程序使用 his 日历创建事件。

做了什么:

  1. 由于 Google Actions 不允许使用 Google Auth/Token 端点,我选择使用 http://www.auth0.com.

  1. auth0.com 上创建了一个帐户(使用我的 Google 帐户),创建了一个应用程序并使用他们的管理面板设置了以下值(Domain, CliendId and ClientSecret auth0生成):

  1. 在 Google 云控制台凭据页面上创建了 OAuth 客户端 ID:

  1. 已配置的操作帐户链接如下:

  1. 返回 auth0.com > 连接 > 社交 > 启用 Google:

  1. 在 DialogFlow > 集成 > Google 助手中检查了 "Sign in required":

  1. 在我的 DialogFlow Webhook 方法的第一行写入日志以记录以下响应:

      {
      "originalRequest":{
         "source":"google",
         "version":"2",
         "data":{
            "isInSandbox":true,
            "surface":{
               "capabilities":[
                  {
                     "name":"actions.capability.AUDIO_OUTPUT"
                  },
                  {
                     "name":"actions.capability.WEB_BROWSER"
                  },
                  {
                     "name":"actions.capability.MEDIA_RESPONSE_AUDIO"
                  },
                  {
                     "name":"actions.capability.SCREEN_OUTPUT"
                  }
               ]
            },
            "inputs":[
               {
                  "rawInputs":[
                     {
                        "query":"test",
                        "inputType":"KEYBOARD"
                     }
                  ],
                  "arguments":[
                     {
                        "rawText":"test",
                        "textValue":"test",
                        "name":"text"
                     }
                  ],
                  "intent":"actions.intent.TEXT"
               }
            ],
            "user":{
               "lastSeen":"2018-05-03T11:40:57Z",
               "accessToken":"4CfRs-Lt5lWVQuyOYODvf1xxxxxxx",
               "locale":"en-US",
               "userId":"15229245xxxxx"
            },
            "conversation":{
               "conversationId":"15253476xxxxx",
               "type":"ACTIVE",
               "conversationToken":"[\"authentication\",\"wh_patient-details\"]"
            },
            "availableSurfaces":[
               {
                  "capabilities":[
                     {
                        "name":"actions.capability.AUDIO_OUTPUT"
                     },
                     {
                        "name":"actions.capability.SCREEN_OUTPUT"
                     }
                  ]
               }
            ]
         }
      },
      "id":"1d6ed865-0803-49ca-bbac-xxxx",
      "timestamp":"2018-05-03T11:42:22.835Z",
      "lang":"en-us",
      "result":{
         "source":"agent",
         "resolvedQuery":"test",
         "speech":"",
         "action":"v00.xxxxx",
         "actionIncomplete":false,
         "parameters":{
            "CallEnum":"Test"
         },
         "contexts":[
            {
               "name":"authentication",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":1
            },
            {
               "name":"actions_capability_screen_output",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":0
            },
            {
               "name":"actions_capability_audio_output",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":0
            },
            {
               "name":"wh_patient-details",
               "parameters":{
                  "patientId":0,
                  "CallEnum":"Test",
                  "fallbackLifespan":0,
                  "providerId":0,
                  "practiceId":0,
                  "CallEnum.original":"",
                  "fullDob":"01 January, 0001"
               },
               "lifespan":199
            },
            {
               "name":"google_assistant_input_type_keyboard",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":0
            },
            {
               "name":"actions_capability_web_browser",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":0
            },
            {
               "name":"actions_capability_media_response_audio",
               "parameters":{
                  "CallEnum":"Test",
                  "CallEnum.original":""
               },
               "lifespan":0
            }
         ],
         "metadata":{
            "intentName":"v00xxxx",
            "isResponseToSlotfilling":false,
            "intentId":"c7bd9113-d5b4-4312-8851-xxxxxxx",
            "webhookUsed":"true",
            "webhookForSlotFillingUsed":"false",
            "nluResponseTime":556
         },
         "fulfillment":{
            "speech":"Test",
            "messages":[
               {
                  "type":0,
                  "speech":"Test"
               }
            ]
         },
         "score":0.8399999737739563
      },
      "status":{
         "code":200,
         "errorType":"success"
      },
      "sessionId":"152534xxxxxxx",
      "isStackdriverLoggingEnabled":false
    

    }

相关部分是:

"user":{
           "lastSeen":"2018-05-03T11:40:57Z",
           "accessToken":"4CfRs-Lt5lWVQuyOYODvf1xxxxxxx",
           "locale":"en-US",
           "userId":"15229245xxxxx"
        }
  1. 来自这个post:

The auth token (which you have issued, because you're the OAuth server) will be sent in the JSON object at originalRequest.data.user.accessToken.

所以我在下面的代码中使用了上面的授权令牌:

string clientId = "361385932727-ksg6jgjxxxxxSNIP";
string clientSecret = "rc2K1UUyntxxxxxxSNIP";
string accessToken = jsonObject.SelectToken("originalRequest.data.user.accessToken");
string userId = jsonObject.SelectToken("originalRequest.data.user.userId");

IAuthorizationCodeFlow flow =
        new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
        {
            ClientSecrets = new ClientSecrets
            {
                ClientId = clientId,
                ClientSecret = clientSecret
            },
            Scopes = new[] { CalendarService.Scope.Calendar }
        });

TokenResponse token = flow.ExchangeCodeForTokenAsync(userId, accessToken, 
    "https://oauth-redirect.googleusercontent.com/r/xxxxxxxxx", CancellationToken.None).Result;

UserCredential credential = new UserCredential(flow, userId, new TokenResponse { AccessToken = token.AccessToken });
CalendarService service = new CalendarService(new BaseClientService.Initializer()
{
    HttpClientInitializer = credential,
    ApplicationName = "Test Auth0",
});

var list = service.CalendarList.List().Execute().Items;

例外情况:

Error:"invalid_grant", Description:"Malformed auth code.", Uri:""
Stacktrace:    at Google.Apis.Auth.OAuth2.Requests.TokenRequestExtenstions.<ExecuteAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<ExchangeCodeForTokenAsync>d__30.MoveNext()

当我将上面代码中的 ClientId/ClientSecret 更改为 auth0.com 中的那个时,异常是:

Error:"invalid_client", Description:"The OAuth client was not found.", Uri:""
Stacktrace:    at Google.Apis.Auth.OAuth2.Requests.TokenRequestExtenstions.<ExecuteAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<ExchangeCodeForTokenAsync>d__30.MoveNext()

我在这里错过了什么?有人可以帮忙吗

这里是 "missing" 的步骤,之后我让这个东西工作了。我确信必须有其他方法可以做到这一点,但在我在这里得到一些反馈之前,我会这样做就足够了。

注意:Google 操作控制台 UI 已更改,因此原始问题中的屏幕可能不同(但完整)。

  1. 确保日历 API 在 Google 云控制台(API 库)中针对您选择的项目启用。

  1. 返回 https://manage.auth0.com > APIs。编辑列出的系统 API > 机器到机器应用程序。授权您列出的应用程序和 select/tick 以下两个范围:

    阅读:用户
    read:user_idp_tokens

  1. 转到 Google 操作控制台 > 帐户链接(参见第 4 点)并检查您是否设置了以下范围:

  1. 通过以下方式更改您的代码:
    一种。使用来自 originalRequest.data.user.accessToken 的授权令牌调用 https://[domain].auth0.com/userinfo(参见第 7 点和第 8 点)。成功的回应应该会给你 userId
    b. https://[domain].auth0.com/oauth/token 上的 Post 请求 body 包含您的 client_id、client_secret、观众 grant_type。成功的回复应该会给你一个新的 access_token.
    C。将授权令牌更改为从点 11.b 新获取的 access_token 并调用 https://[domain].auth0.com/api/v2/users/[userId],其中 userId 是您从点 11.a 获得的那个。成功的响应应该给你一个 "Google" access_token 和 userId(在身份下)。
    d.将 header 中的授权令牌更改为点 11.c 中的授权令牌。这是您用来调用 Google API 的令牌。例如,对于日历,调用 https://www.googleapis.com/calendar/v3/users/me/calendarList。您将得到所需的答复。

这是代码(我已经重用了变量):

string responseText = string.Empty;
string clientId = "DCuPWHknmv_k2xxxxxxxxxxxxxxxxx";     //Auth0 ClientId
string clientSecret = "7G3xlreFIULPZ9OtwlOxCX99xxxxxxxxxxxxxxxxxxx";    //Auth0 ClientSecret
string accessToken = jsonObject.SelectToken("originalRequest.data.user.accessToken").ToString();

try
{
    using (var httpClient = new HttpClient())
    {
        httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
        var url = "https://xxx.auth0.com/userinfo";
        responseText = httpClient.GetStringAsync(url).Result;

        JObject jsonUserInfo = JObject.Parse(responseText);
        string userId = jsonUserInfo.SelectToken("sub").ToString();

        url = "https://xxx.auth0.com/oauth/token";
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("client_id", clientId),
            new KeyValuePair<string, string>("client_secret", clientSecret),
            new KeyValuePair<string, string>("audience", "https://[domain].auth0.com/api/v2/"),
            new KeyValuePair<string, string>("grant_type", "client_credentials")
        });

        var postResult = httpClient.PostAsync(url, content).Result;
        jsonUserInfo = JObject.Parse(postResult.Content.ReadAsStringAsync().Result);
        accessToken = jsonUserInfo.SelectToken("access_token").ToString();

        httpClient.DefaultRequestHeaders.Remove("Authorization");
        httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);

        url = "https://xxx.auth0.com/api/v2/users/" + userId;
        jsonUserInfo = JObject.Parse(httpClient.GetStringAsync(url).Result);
        accessToken = jsonUserInfo.SelectToken("identities[0].access_token").ToString();
        userId = jsonUserInfo.SelectToken("identities[0].user_id").ToString();

        httpClient.DefaultRequestHeaders.Remove("Authorization");
        httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);

        url = "https://www.googleapis.com/calendar/v3/users/me/calendarList";
        responseText = httpClient.GetStringAsync(url).Result;
    }
}