Rails APIモードで開発するときの認証用のトークンはどこに保存すればいいの問題

越川佳祐氏:私からは、「Rails APIモードにおけるToken認証機能について」というテーマでLT(ライトニングトーク)をしようと思っていたんですが、スライドを作っていて「あれ、これ別にRailsだけの話じゃなくない?」と思ってしまいました。みなさんの中にも、そう思う方がいるかもしれないんですが、もうこれで作っちゃったのでご了承ください。

私は株式会社iCAREで、サーバーサイドエンジニアをしている、越川と申します。Twitterは@kossy0701という名前でやっているので、よければフォローをしてもらえればと思います。

突然なんですが、みなさんこんな経験はありませんか? 「Rails APIモードで何か開発したいな、認証機能どうしようかな、devise_token_authというトークンベースの認証方式を提供するGemがあるのか。CORSという仕組みの設定が必要なのか。rack-corsというGemなら簡単にCORSの設定ができるのか。rails g devise_token_auth:install User authすれば簡単に導入できるのか。Postmanを使えば普通にAPIのテストもできるのか……」という流れを、僕も何回か経験しているんですが、APIモードだとCookieの仕組みがデフォルトで入っていないので、デバイスじゃなくてdevise_token_authというGemを使うことがけっこう多いのかなと思っています。

あとはCross-Origin Resource Sharingなんですが、これも例えばRailsをlocalhost:3000で、Vueをlocalhost:8080でと2つサーバーを立ち上げて試すというときは、異なるオリジンで通信する必要があるので、CORSの設定をrack-corsでやることがけっこう多いんじゃないかなと思っています。

Postmanは無料で試せるAPIのクライアントですね。これも便利なのでみなさん使ってみてください。ここで矢印が終わっているんですが、認証用のトークンはどこに保存すればいいの? という疑問にたどり着くんですね。

僕もたどり着いたので、今回「SPAの認証Tokenどこに保存するの問題」を話していきたいと思います。これは別にRailsと関係はなくない? といったところなんですが、すみません、ここはご了承ください。

ローカルストレージ保存はトークンが簡単に盗まれてしまう

認証トークンの保存先として、ローカルストレージと、Cookieと、インメモリがよく挙がる名前だと思っています。順に上から説明していきます。

選択肢その1として、ローカルストレージに保存するというシナリオですね。この場合は個人的には実装が簡単だと思っています。これはdevise_token_authの例ですが、ログインのAPIを叩いたら、ヘッダーの中にアクセストークンやuidやclientが入っていて、その中からローカルストレージにsetItemでゴリゴリ入れていきます。

認証が必要なエンドポイントを叩くときに、リクエストヘッダーにgetItemでトークンを取り出して、乗せて、リクエストするだけです。ログアウトするときはローカルストレージに入れていたトークンをremoveItemでそのまま削除すればOKで、ログアウトした後に、ログインページに遷移させるというフローだと思うんです。

最低限で実現するんだったら、これだけでいいのかなと思うんですが、みなさんご存知かもしれませんが、クロスサイトスクリプティングがあった場合、JavaScriptで操作しているものなので、トークンを簡単に盗めるんですね。

Auth0という認証プラットフォームの会社が、CDN経由にJavaScriptの脆弱性があってこれが盗めてしまうとドキュメントを出していて、わりと有名な問題です。

なので、例えばどうしてもローカルストレージに保存しなきゃいけないときは、トークンの有効期限そのものをかなり短くしたり、devise_token_authでリクエストごとにトークンを更新したり、XSSがあることを前提に認証まわりの実装をするのがいいんじゃないかなと思っています。

Cookieはクロスサイトスクリプティングの脆弱性がある

次はCookieですね。選択肢その2のCookieに保存するというものです。CookieにHttpOnly属性というものがあって、これをfalseにするとJavaScriptから操作ができます。これをtrueにしておくことで、JavaScriptからのCookie操作を無効にできるのでローカルストレージよりセキュアです。

ローカルストレージは、JavaScriptでゴリゴリ操作できちゃうので、そういう意味では(Cookieへの保存は)セキュアかなとは思っているんですが、これも懸念点はあります。セキュリティでかなり有名な徳丸浩さんが、「HttpOnly属性を仮にtrueにしていても、クロスサイトスクリプティングの脆弱性があるとcookieは盗めませんが、他の方法により攻撃ができる」ということを言及していました。

クロスサイトスクリプティング対策として、CookieのHttpOnly属性がどこまで安全になるのかですね。徳丸さんがCross-Origin Resource Sharingの話をしている動画があるので、ご覧いただければと思います。

(編注;スライドは発表当時のものであり、徳丸氏の発言とは異なります。)

インメモリ保存はタブ間でログイン状態が保持されないのが問題

選択肢その3のインメモリで保存する方法ですね。インメモリは何かというと、JavaScriptでブラウザのメモリ内に認証トークンを保存するというやり方です。例えばクロージャーみたいなものを作ってそこに入れるなど、やり方がいろいろとあって、これだと攻撃者からはどこにトークンが存在するかがわからないので、攻撃が困難だと言われています。

これも一見セキュアですが、懸念はあります。メモリ内にトークンを保存するので、例えばリロードするとログアウトされちゃうという問題です。ほかにも、ブラウザで新しいタブで開いたときに認証状態が継続されなくて、タブ間でもログイン状態が続かないので永続性がない、ログアウト状態になってしまうという問題があります。

第4の選択肢Auth0

3つともダメやんと思うかもしれませんが、ここで選択肢その4が出てきます。先ほどチラッとお話ししたんですが、Auth0という認証プラットフォームです。アメリカの会社なんですが、Webアプリやモバイル向け、API向けの認証や認可の仕組みを提供している、いわゆるIDaaSのプラットフォームです。誤解を恐れずに言うと、自前で認証認可の仕組みを実装したくない人向けのものだと思っています。

オプションで、2段階認証やSAMLの認証も提供しているのでけっこう便利なんじゃないかなと思っています。Auth0の何がいいのかと言うと、Silent Authenticationという仕組みがあって、インメモリ形式で実装しているんですが、リロード時にログアウトしてしまう問題がないのが利点として挙げられます。

これがAuth0のドキュメントで、『Configure Silent Authentication』とドキュメントがあるので、興味があればぜひ読んでいただきたいと思います。

どういう中身の実装になっているのかというと、HTMLの<iframe>というタグを酷使して、Silent Authenticationを実現しています。/auth0-spa-jsというGitHubのリポジトリ(https://github.com/auth0/auth0-spa-js)があって、僕もそれをがんばって読もうとして挫折したんですが(笑)。

興味がある人は、GitHubにオープンソースとして公開されているので、実装を確認してもらえればと思います。Auth0は、ドキュメントも充実していて、例えばこれはRailsの6でAuth0を使って認証APIを作るみたいなハンズオンなんですが、これもドキュメントがあります。ほかにも、フロント側もQuickStart Vue:Loginという、最低限こう実装すればできるみたいなチュートリアルも用意されていたり、スライドには書いていないんですが、モバイルアプリ向けのドキュメントも充実しています。

Auth0はユーザーが増加したときにコストがかかる

ここで懸念の話なんですが、これはセキュリティの話から離れてしまって恐縮なんですが、認証基盤のロックインという問題はあると思っています。例えば、Auth0がいいなと思って使い始めて、ユーザー数が増えてきたからコスト的にもキツイな、やめたいなとな、移行したいな、となったときに、その移行作業が大変だったり、単純にAuth0のサービスが停止してしまうというリスクもあると思うので、そういったところでAuth0だけに頼るのも難しいのかなと思っています。あとは、ユーザー数が一定数を超えると課金が発生するので、SNSなどで広がってユーザーが1万人とかになってくると、けっこう大変なんじゃないかなと思っています。

これがプライシングの画像なんですが、ここにも書いてあるとおり、5,000人のユーザーで114ドル/マンスリーの料金プランとなっているので、個人開発などであまりユーザーが増えないならいいかなと思います。が、Auth0は採用する前に、どれぐらいユーザー数が増えて、将来これぐらいのコストがかかるというところまで考えるべきだと思っています。

認証トークンの保存先は扱う情報の機密性に応じて決定する

結局認証トークンをどこに保存するかは、アプリで扱う情報の機微性に応じて保管場所を決めるべきというありきたりな結論になってしまったんですけど(笑)。

例えば社内の書籍管理アプリみたいな最悪外部に出てもいいぐらいのレベル感だったら、ローカルストレージに保存してもいいと思うんですが、堅牢情報や決済情報など機密性の高い情報を扱うのであれば、セキュアな認証方式や認証トークンの保存場所を考える必要があると思っています。

ご清聴ありがとうございました。

※[編注]登壇者の要望を受け、一部加筆修正を行いました。(2021年5月21日 17:30)
誤)徳丸浩さんが、HttpOnly属性を仮にtrueにしていても、クロスサイトスクリプティングの脆弱性があると、XMLHttpRequestの実行でそのCookie内のトークンを盗むことができてしまうということを言及していました。

正)徳丸浩さんが、「HttpOnly属性を仮にtrueにしていても、クロスサイトスクリプティングの脆弱性があると、cookieは盗めませんが、他の方法により攻撃ができる」ということを言及していました。