GoogleHome
actionsongoogle

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

はじめに

はじめまして。

動機

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

最初に試したこと。

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

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

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

Google公式によるuser IDの説明

まずは素直に2Googleの公式説明を読みます。
https://developers.google.com/actions/identity/user-info

大まかに言って、「匿名の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人3という制限がありますし、使用間隔が30日以上空いてしまうことが想定されるアプリでも問題になります。

OK Google, Voice Matchを使わずにユーザーを識別する方法を教えて?

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

Google assistant でGoogleのアカウントの情報を取得する

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

課題1 Authorization URLにGoogleのURLは使えない。

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

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

課題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のポリシーにより、認可サーバーを使えない問題もありまして……。
楽天レシピのように、音声だけで「メールアドレスを利用して良いですか?」と確認出来たらとても便利なのですけど。6

課題3 認証が保持される時間

さらに、数時間してから試してみたところと、

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

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

注文した覚えのないidToken

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

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

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

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のことだったようで、中身は下記のような感じでした。

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送ってくるんですか?

まとめ

謎はさらに深まった。


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

  2. といってもここにたどり着くまでにいろいろさまよったのですが。 

  3. 1 台の Google Home で Voice Match を使用できるユーザーは、最大 6 人です。 

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

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

  6. たぶんそれを実現する方法がStreamlined Identity flowなのでしょうか。