ちょっと話題の記事

ユーザーをログアウトから守れ!―シーケンス図から読み解くログイン状態維持【Mobileアプリ編】

認証というのは面倒なもので、利用者に余計な手間を掛けさせてアクティブ率を下げたくないと日夜工夫を凝らす我々にとっては、やり玉に上がりやすいテーマであると思います。要するに、ユーザーをログアウトさせたくないわけです。さて、どうしましょう?

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

生魚おじさん、都元です。今日の魚はワラサです! 出世魚であるブリのちっちゃいヤツです!

今回はユーザーをログアウトから守れ!―シーケンス図から読み解くログイン状態維持【Webアプリ編】と対になる、Mobileアプリ編をお送りします。

さて、認証というのは面倒なもので、利用者に余計な手間を掛けさせてアクティブ率を下げたくないと日夜工夫を凝らす我々にとっては、やり玉に上がりやすいテーマであると思います。要するに、ユーザーをログアウトさせたくないわけです。(再掲)

例えば Facebook や Twitter のモバイルアプリはいつ起動しても自分のアカウントでログイン状態になっています。 最後にログインしたのはいつでしたっけ? 覚えていませんよね? これがおそらく皆さんの理想です。

前回 (Webアプリ編) の応用

モバイルアプリであっても、セッション cookie を使ったログイン状態管理はできると思います。ただ、これでは「ログイン状態維持」ができない、というのも同様です。また、自動ログイン cookie を使うこともできそうです。この辺りは Web アプリ編と全く同じですので、シーケンスは割愛します。

しかし、モバイルアプリに対する API 側は、できればステートレスに実装したいという引力もあるでしょう。ということで、モバイルアプリ特有の方式をいくつか考えていきたいとおもいます。

BASIC認証を使ったMobileAppのログイン状態維持

え、いまどき BASIC 認証ですか、って思いましたか? まぁそりゃそうですよねw BASIC 認証は、全てのリクエストに毎回 ID / pass が流れるため、Attack window が大きいという問題等が確かにあります。

しかし、散々 dis られてはいるものの、きちんと HTTPS を使うことや、キーローテーションなど適切に運用している場合については、BASIC 認証を否定する決定的な理由はありません。そもそも (サーバー間通信ではありますが) OAuth 2.0 では部分的に BASIC 認証をつかっていますしね。 この辺り、興味がある方は私の過去スライド Spring Day 2016 - Web API アクセス制御の最適解 の 15 ページも参照してみてください。

Web アプリとしてブラウザに向けたサーバーを作る場合に BASIC 認証を採用すると、パスワード入力のための UI がダサいとか、ログアウトはどうやるんだとか、様々な問題を抱えることになります。しかし、モバイルアプリに対するバックエンド API としてのサーバーを作る場合は、ブラウザ向けとは違って、ありえない選択肢ではなくなってきます。

まぁ実際のところは、決して BASIC 認証の利用は推奨しませんが、まずは基本として押さえていきたいと思います。

  1. まず、初めてモバイルアプリを起動した状態を考えます。
  2. モバイルアプリは、当然このユーザーが誰なのか、判断できません。
  3. そのため、ユーザーにはログイン画面を提示します。
  4. ユーザーは ID とパスワードを入力して送信します。
  5. その上で、その ID とパスワードを使った Authorization ヘッダを付けて API を呼び出します。
  6. バックエンドサーバーはこれを検証し、認証します。
  7. そのユーザーに向けたコンテンツを返します。
  8. モバイルアプリは、API リクエスト成功をもってログインと判断し、その ID とパスワードを安全なローカルストレージに保存します。
  9. その上でユーザーにコンテンツを提示します。
  10. 以降の操作では、保存した ID とパスワードを取り出して BASIC 認証で API を叩き続けます。

シンプルですね。ID / pass は、変更しない限りずっと使えるはずなので、ログアウトしてしまう心配はありません。 ログインにもし有効期限を付けたいのであれば、ローカルストレージに認証日時や最終利用日時を記録しておき、期限が切れたらパスワードを削除してしまえばいいのです。

自前実装の OAuth を使った MobileApp のログイン状態維持

そのアプリ独自で認証のしくみを実装して利用する場合であっても、OAuth 2.0 に則ってクライアントとバックエンドを設計することは多いと思います。

このような場合権限委譲という OAuth 本来の目的からはズレますが、Resource Owner Credentials Password Grant という OAuth のフローを使って認証をすることが多いと思います。このフローを使うことにより、ID / pass が毎回のリクエストに流れることはなくなります。代わりにアクセストークンが流れますが、これは一定期間後に期限切れとなるため比較的安全である、という考え方です。

ここでは、OAuth 認可サーバーとリソースサーバー (独自API = MobileBackend) を1つのサーバーとして実装している前提でシーケンス図を追って行きましょう。

  1. まず、初めてモバイルアプリを起動した状態を考えます。
  2. モバイルアプリは、当然このユーザーが誰なのか、判断できません。
  3. そのため、ユーザーにはログイン画面を提示します。
  4. ユーザーは ID とパスワードを入力して送信します。
  5. モバイルアプリはバックエンドサーバーに OAuth 2.0 の Resource Owner Credentials Password Grant として Token Request を行います。この時、ID とパスワードを送信しています。
  6. バックエンドサーバーはこれを検証し、認証します。
  7. バックエンドサーバーは Token Response としてアクセストークンとリフレッシュトークンを返します。
  8. モバイルアプリは、これをもってログインと判断し、これらのトークンを安全なローカルストレージに保存します。
  9. さらにモバイルアプリは、アクセストークンを使って API リクエストを行います。
  10. アクセストークンは都度バックエンドサーバーで検証し、都度認証を行います。
  11. そのユーザーに向けたコンテンツを返します。
  12. 最終的にユーザーに向けて、コンテンツを提示します。
  13. その後のユーザー操作においては、
  14. 保存してあるアクセストークンを利用して、
  15. API リクエストを行います。
  16. やはりバックエンドは都度アクセストークンの検証を行ってから、
  17. コンテンツを返します。
  18. それをユーザーに提示します。
  19. その後ユーザーはモバイルアプリから離脱し、アクセストークンの期限切れが発生します。バックエンドサーバーではアクセストークンの破棄を行います。
  20. ユーザーが再びモバイルアプリを起動したとき
  21. 保存してあるアクセストークンを利用して、
  22. API リクエストを行いますが、
  23. アクセストークンの検証には失敗するため、
  24. 401 Unauthorized を返すことになります。
  25. そこでモバイルアプリは保存したリフレッシュトークン取り出し、
  26. これを使ってリフレッシュ手続き (Token Request) を行います。この時、ID とパスワードは不要です。
  27. バックエンドサーバーはリフレッシュトークンを検証し、
  28. それに基づいて、新しいアクセストークンと、新しいリフレッシュトークン (※) を返します。
  29. このトークンを上書き保存し、
  30. 残りは15〜18番のステップと全く同じようにコンテンツを返します。

上記のステップ 28 (※) では「新しいリフレッシュトークンを返す」としましたが、ここで新しいリフレッシュトークンを返すかどうかは、 OAuth サーバーの実装に依存します。OAuth の仕様としては未定義です。

しかしログイン状態維持という視点で見ると、この挙動は重要な役割を果たします。OAuth 2.0 のトークンというのは、一般的に (これも実装依存ではありますが) 一度発行したアクセストークンおよびリフレッシュトークンは延命しません。つまり、生まれた瞬間に寿命が決まっていて、これが伸びることはありません。revokeによって縮むことはあるかもしれませんが。

従って、リフレッシュ手続き時に「新しいリフレッシュトークンを返す」かどうかで、ログイン状態維持が実現できるかどうかが決まります。新しいリフレッシュトークンを返すのであれば、定期的にリフレッシュ手続きを続けている限り、永遠にログイン状態を維持できます。しかし、新しいリフレッシュトークンを返さない場合は、いつかそのリフレッシュトークンは期限切れとなってしまいます。

ここは要件次第で決めましょう。定期的なアクセスを続けている限り永遠にログイン状態を維持したいのであれば新しいリフレッシュトークンを発行するようにしましょう。逆に、どれだけ定期的に使っていても、最後の認証から一定時間が経過したら再度認証を行いたい場合は、新しいリフレッシュトークンは返さないように作りましょう。

アクセストークンとリフレッシュトークンの有効期間の決め方

さて、ではアクセストークンとリフレッシュトークン有効期間というのはどのように決めたらいいのでしょうか? ちなみに前述の通り、これらの有効期限はトークンの発行から一定期間で切れます。「最終利用から」ではないことに注意しましょう。

これらの有効期限を決めるために、まずはモバイルアプリに対するユーザーの挙動として、次のような統計情報を用意します。

  • 滞在時間 (アプリ起動から離脱までの時間)
  • 滞在中のリクエスト間隔 (前回のレスポンスを返してから、次のリクエストを送るまでの時間)
  • 再起動間隔 (前回の離脱から、次のアプリ起動までの時間)

アクセストークンの有効期間は、「滞在時間」の 90 パーセンタイル辺り... と言いたいところですが、まぁそんな難しく考えず、30 分決め打ちでいいような気もします。切れちゃっても、リフレッシュできるわけですから。

次にリフレッシュトークンの有効期間ですが、これは例えば「再訪問間隔」の 90〜99.9 パーセンタイル辺りを狙っていきましょう。つまり 0.1〜10 パーセントのユーザーが、再起動時に再認証を求められる、というような計算です。

全員を救うことはできません。であれば、何パーセント救えばいいですか? という議論をするのです。

あ、滞在中のリクエスト間隔は関係ありませんでしたね :P

ソーシャルの OAuth を使った MobileApp のログイン状態維持

次に。やはり最近の流れでは自前で OAuth 認可サーバーを実装する機会もそう多くはありません。大抵の場合は Facebook や Twitter 等、大手の IdP (IDプロバイダー) を利用することでしょう。というわけで、IdP を絡ませたフローを見ていきましょう。

  1. まず、初めてモバイルアプリを起動した状態を考えます。
  2. モバイルアプリは、当然このユーザーが誰なのか、判断できません。
  3. そのため、アプリは IdP を開きます。ここは、代表的には 2 つの実装パターンがあります。
    • IdP に対して OAuth 2.0 Authorization Request を行うように Web ビュー等のブラウザを開くパターン
    • IdP の公式アプリ (Facebookアプリ等) を開くパターン
  4. 結果的に、モバイル端末は IdP に対して Authorization Request (またはそれ相当のアクション) を行います。
  5. ブラウザを開くパターンの場合大抵はログイン状態にありませんので、ここでログインフォームを返します。アプリを開くパターンでは大抵ログイン状態だと思うので、8番まで飛びます。
  6. ユーザーは IdP の ID とパスワードを入力して送信します。
  7. IdP はパスワードを検証して認証します。
  8. 認証に問題がなければ、ここで再び自分たちのモバイルアプリに戻ってきます。
    • Web ビュー等のブラウザを開いたパターンでは、ブラウザが閉じるだけです。
    • IdP の公式アプリを開いた場合は、元のモバイルアプリに戻ってきます。
  9. 元のアプリは、IdP が発行した Authorization code を手に入れます。
  10. 手に入れた Authorization code を IdP に送信します。 (Token Request)
  11. バックエンドサーバーは Token Response としてアクセストークンとリフレッシュトークンを返します。
  12. モバイルアプリは、これをもってログインと判断し、これらのトークンを安全なローカルストレージに保存します。
  13. さらにモバイルアプリは、アクセストークンを使って API リクエストを行います。
  14. アクセストークンを受け取ったバックエンドサーバーは、このトークンの正当性を IdP に問い合わせます。ただし、この実装方法は以前のブログOAuth 認証を真面目に考えるで述べた通り、妥当かどうかの判断は難しいです。ここでは、正しいと割り切ってしまいます。
  15. トークンの検証に成功すると、IdP 上の username が手に入ります。
  16. バックエンドサーバーはその username を完全に信頼して構いません。これを認証として扱います。
  17. そのユーザーに向けたコンテンツを返します。
  18. 最終的にユーザーに向けて、コンテンツを提示します。
  19. その後のユーザー操作においては、
  20. 保存してあるアクセストークンを利用して、API リクエストを行います。
  21. バックエンドサーバーはアクセストークンの有効性を知る術が無いので、毎回 IdP に問い合わせを行います。(ただし、一定の妥協をして結果をキャッシュすることも必要かもしれません。)
  22. トークンの検証に成功すると、IdP 上の username が手に入ります。
  23. バックエンドサーバーはその username を完全に信頼して構いません。これを認証として扱います。
  24. そのユーザーに向けたコンテンツを返します。
  25. 最終的にユーザーに向けて、コンテンツを提示します。
  26. その後ユーザーはモバイルアプリから離脱し、アクセストークンの期限切れが発生します。IdP ではアクセストークンの破棄を行います。
  27. ユーザーが再びモバイルアプリを起動したとき
  28. 保存してあるアクセストークンを利用して API リクエストを行いますが、
  29. アクセストークンの検証には失敗するため、
  30. inactive レスポンスを返すことになります。
  31. これではバックエンドサーバーも手が出せませんので、401 Unauthorized 辺りのレスポンスを返すことになります。
  32. そこでモバイルアプリは保存したリフレッシュトークン取り出し、リフレッシュ手続き (Token Request) を行います。この時、ID とパスワードは不要です。
  33. IdP は新しいアクセストークンと、新しいリフレッシュトークン (※) を返します。
  34. このトークンを上書き保存し、
  35. 残りは13〜18番のステップと全く同じようにコンテンツを返します。

まとめ

と、いうわけで。ユーザーをログアウトさせたくない! という要件を満たすために、いくつかの実装方法を見てきました。

また、本稿でお伝えしたいことは、具体的なシーケンスもさることながら、アクセストークンとリフレッシュトークンの有効期間の決め方でありました。

繰り返しになりますが、全員を「ログアウト」から救うことはできません。なので、何パーセントを救えばいいのか、という視点で考えることが重要です。