はじめに
はじめまして。
動機
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が取得できました。
{
"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のことだったようで、中身は下記のような感じでした。
{
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送ってくるんですか?
まとめ
謎はさらに深まった。
-
(2018/1/28追記)この情報を確認するだけなら、AWS Lambdaで受け取ったログを確認するまでもなく、Actions on GoogleのシミュレーターのDEBUGタブ見た方が早いですね。 ↩
-
といってもここにたどり着くまでにいろいろさまよったのですが。 ↩
-
なんでそんなポリシー変更したんですかGoogle先生?どこかに理由を解説しているドキュメントは無いんですか? ↩
-
それかTwitterとかMSとかのOAuthサーバーを使う手もありそうなんですが、ユーザーが折角Googleアカウントにログインしているというのに、他のサービスのアカウントと連携させるというのもちょっとどうかと…) ↩
-
たぶんそれを実現する方法がStreamlined Identity flowなのでしょうか。 ↩