Google Assistantアプリでユーザー識別情報を取得したい

# はじめに
はじめまして。

# 動機
Google Home用Google Assistant用のアプリを開発していて、誰が話しているのかを特定したくなりました。
例えばIoTデバイスを操作しようと思った場合、開発環境内だけで個人的に遊ぶアプリなら良いですけど、開発したアプリを公開するためには、何らかのユーザー識別情報が必要です。
ということを考えると、Google Homeを使うにあたってGoogleアカウント情報を取得出来たら便利です。
……便利なのです、が、まだ上手くいっておりません。
なのでここにあるのは、現在私が躓いているポイントを記したメモになります。


# 最初に試したこと。
予めAWS LambdaとかでWebAPIを用意しておき、DialogflowからWebhookでそのWebAPIにアクセスするとします。その時WebAPIに送信されるJSONデータの中にuserという項目が入っています。[^6]
こんな感じ：

```JSONBareObject:
{ ..., 
  user: {
    "lastSeen":"2018-01-01T01:23:45Z",
    "locale":"ja-JP",
    "userId:"hogehoge"
  },
  ...
}
```

最初はこのuserIdでユーザーを識別できるじゃないかと気楽に考えたのですが、そう簡単には行きません。
このuserId、シミュレーター使った時とGoogle Homeデバイスから話しかけた時とで値が変わったりするのです。

[^6]: (2018/1/28追記)この情報を確認するだけなら、AWS Lambdaで受け取ったログを確認するまでもなく、Actions on GoogleのシミュレーターのDEBUGタブ見た方が早いですね。

# Google公式によるuser IDの説明
まずは素直に[^1]Googleの公式説明を読みます。
https://developers.google.com/actions/identity/user-info

[^1]: といってもここにたどり着くまでにいろいろさまよったのですが。

大まかに言って、「匿名のuserID」と、「ユーザーに名前を要求する方法」があるよ、とのこと。
まず「匿名のuserID」の詳細を見ると下記のように分類されています。。

> * Voice-activated speakers
>  * When a voice matches
>  * When no voice matches
> * Android mobile devices
> * Actions Console Simulator
>  * Phone Surface
>  * Speaker Surface

続いてLanguagesとUser ID lifetimeの項目がありますが、これは共通説明のようですね。

> * Languages ― UserIDは言語を跨っても同一です。
> * User ID lifetime ― 30日間使われなかったり、ユーザーがデバイスとアカウントを切り離したりすると、UserIDはリセットされます。

で、最初の疑問に対する答えですが、Actions Console Simulator の項目を読むと、ちゃんと書いてありますね。スピーカー使うと別のIDになるようです。
それは解りました。
でも重要なのは、アプリ公開後にユーザー識別できるかどうかです。メインで考えているのはGoogle Homeスピーカーによる操作ですので、Voice-activated speakersのところをよく読みます。
すると…

> * When a voice matches: デバイスに登録済みの声とマッチした場合、不変のuserIDが返る。登録されている音声プロファイルごとに、別々のUserIDになる。このUserIDは、そのデバイスで行われるあらゆる会話に関連づけられる。
> * When no voice matches: デバイスに声を登録していなかったり、誰の声か分からなかった場合、異なるIDになる。このIDはその会話においてのみユニークである。

つまり、Voice Matchを使わない場合、会話セッションが変わっても同じIDが使われるかどうかは保証されないということですね。
ということは、会話セッションをまたいでも変らないUserIDを使いたい場合、Voice Match機能で声を登録するしかない、ということです。
では、Voice Matchを使わない環境ではどうしましょう？
何しろGoogle HomeのVoice Matchを登録できるユーザーは最大6人[^2]という制限がありますし、使用間隔が30日以上空いてしまうことが想定されるアプリでも問題になります。

[^2]: [1 台の Google Home で Voice Match を使用できるユーザーは、最大 6 人です。](https://support.google.com/googlehome/answer/7323910?hl=ja )

# OK Google, Voice Matchを使わずにユーザーを識別する方法を教えて？
Google Home自体を識別する情報を探してみたけど見つからないので、Googleアカウントを使うことを考えます。
Google Homeを使える環境になっている以上、Googleアカウントは紐づけられているはず。
というわけでたどり着いたのがこちらの記事。

[Google assistant でGoogleのアカウントの情報を取得する](https://qiita.com/flatfisher/items/1f39de99728d8717c73b)

これで万事解決……とはなりません。
実際にやってみると、少なくとも3つの課題に直面しました。


## 課題１ Authorization URLにGoogleのURLは使えない。
まずはAuthorization URL。
手順に従って https://accounts.google.com/o/oauth2/v2/auth と入力してしまうと、Googleのポリシー違反で怒られてしまい、ボタンが押せず、次のステップに進めません。
Googleではなく自分のサービスでOAuthの認可サーバーを作れとのこと。Googleのポリシーが変わったのだから仕方がありません。[^3]

[^3]: なんでそんなポリシー変更したんですかGoogle先生？どこかに理由を解説しているドキュメントは無いんですか？

ただ、StackOverflowとかで探してみると、適当なURLを入力して一旦保存し、あとから編集すれば良いとのこと。やってみると、本当にGoogleの認可サーバーを入力できました。わーい。
……。
いやいやダメでしょう。これ間違いなく公開時の審査でひっかかりますって。
横着せずにOAuth 2.0の認可サーバーを立てなきゃダメってことですね。[^4]
でも、Google Driveにアクセスするようなアプリの場合、どうやって解決しているのでしょう？

[^4]: それかTwitterとかMSとかのOAuthサーバーを使う手もありそうなんですが、ユーザーが折角Googleアカウントにログインしているというのに、他のサービスのアカウントと連携させるというのもちょっとどうかと…)

## 課題2 ユーザー認証のためにシミュレーターのDEBUG欄を開くの？
2つ目の問題は、認可の方法です。
読み進めていくと、シミュレーターのDEBUGタブに書かれているURLをコピペしてブラウザでアクセスするという手順が出現します。
当然「そんな手順、どうやってユーザーに実行させるの？」というぎもんが出てきますよね。
そんな時はGoogleアシスタントアプリで試してみましょう。するとこんな感じになります。

>ユーザー「OK Google, xxxアプリにつないで」
>Google「XXXアプリがまだリンクされていません。」
>Google「XXXアプリをGoogleアカウントにリンク」

最後のはボタンです。このボタンを押すと、Googleアカウントのログインページ(あるいはアカウント選択ページ)が表示されますので、Googleアカウント(使用中のGoogleアシスタントとは別のアカウントでも良い)でログインして、ID連携を許可することができます。

これを実行した後なら、Google Homeからでも当該アプリを使えるようになります。
上手くいかないときは、Google Homeから、Google Homeアプリで何か設定しろと言われるますので、設定します。(TODO:確認したら追記)

ただしこの手順はまだユーザーにとって手間なので、Google Homeデバイスだけでメールアドレスを取得したいところです。というか、予想はつくと思いますが、この手順で取得できるのはGoogleHomeを使っているユーザーのGoogleアカウントではなく、連携した OAuth2.0サーバーになってしまうんですよね。
課題1で述べた通り、Googleのポリシーにより、認可サーバーを使えない問題もありまして……。
楽天レシピのように、音声だけで「メールアドレスを利用して良いですか？」と確認出来たらとても便利なのですけど。[^5]

[^5]: たぶんそれを実現する方法が[Streamlined Identity flow](https://developers.google.com/actions/identity/oauth2-assertion-flow)なのでしょうか。

## 課題3 認証が保持される時間
さらに、数時間してから試してみたところと、

>「xxxアプリがまだリンクされていません」

と言われまいました。
Implicit flowではなく、Authorization Code flowを選んでも同じです。
やはり、Googleアカウントを使っているのが悪いのでしょうか？
Google Homeスピーカーだけでは認証できないので、このままだと毎回googleアシスタントを立ち上げてもらう必要が出てきます。困った困った。

# 注文した覚えのないidToken
ちなみに、上述の内容を試してみると、userIdのところは下記のような感じのデータになります。

```JSONBareObject:
{ ..., 
  user: {
    "lastSeen":"2018-01-01T01:23:45Z",
    "idToken":"超長い文字列",
    "accessToken":"それなりに長い文字列",
    "locale":"ja-JP",
    "userId:"hogehoge"
  },
  ...
}
```

"accessToken"を使って、参照した解説記事の通りにgoogleのAPIを叩いてみると、無事にemail情報の入ったJSONが取得できました。

```JSONBareObject:accessTokenを使って取得したJSONデータ
{
 "id": "*********************",
 "email": "b***@gmail.com",
 "verified_email": true,
 "name": "",
 "given_name": "",
 "family_name": "",
 "picture": "https://****/****.jpg",
 "hd": "******************"
}
```

ちなみにこの b\*\*\*@gmail.com というのは当然ですが、OAuthの際に入力したほうのGoogleアカウント情報です。

それは良いのですが、ついでに何故か"idToken"も追加されています。いろいろ調べてみた末に、Base64でデコードしてみたところ、OpenID ConnectのidTokenのことだったようで、中身は下記のような感じでした。

```JSONBareObject:idTokenのペイロード
{ 
  azp:"************.apps.googleusercontent.com",
  sub:"*********************",
  email:"a***@gmail.com",
  evail_verified:true,
  exp:**********",
  iss:"https://accounts.google.com",
  jti:"***************************",
  iat:"**********",
  nbf:"**********"
}
```

「ははーん、なるほどGoogleの OAuthサーバーに対してaccessTokenだけじゃなくてidTokenも取得してくれたんだな、このお人よしめ」と期待しますが、emailの項目がおかしい。
accessTokenで取得したのは b\*\*\*@gmail.com でしたが、こちらは a\*\*\*@gmail.com です。
これは Actions on Googleの開発に使っているアカウント、つまりテストに使ったGoogleアシスタントおよびGoogle HomeスピーカーのGoogleアカウントなんですよね。

え、なにこれ。つまりどういうことなんです？
確かに欲しかったのはそのメールアドレスなんですけど、どうしてその情報を送るのにOAuthが必要なんですか？

(追記 2018/1/28)
Dialogflow側で、"Signe in required"のチェックをオフにすると、当然accessTokenは取得されなくなります。
…が、idTokenは送られてきます。
新しくAgentを作って試してみてもidTokenが送信されることはないので、Googleさんが仕様変更したわけでもなさそう。
どういう条件でidToken送ってくるんですか？

# まとめ
謎はさらに深まった。