Rails APIにおける永続的セッション情報の保存方法

Ruby on Rails Advent Calendar 2022の2日目の記事です。
Advent Calendar初参加です、よろしくお願いします。

このブログで普段はプログラミングに関する記事は書かないんですけど、個人開発で悩んだことがあったので、記録に残しておこうと思います。

ユーザー管理でDeviseを使うときにDevise_token_authを使うべきかどうか悩んだときの記録です。

実務経験は皆無なので、詳しい方いましたらコメント大歓迎です(返信できるかはわかりませんが…)

はじめに

メモ
この記事で言う「永続的セッション」とは、ページを閉じてもユーザーのログイン状態を保持することです。
HTTPプロトコル自体はステートレス(アクセスしたユーザーが同一ユーザーかどうか知らない)ので、何らかの方法でユーザーを識別する情報(セッション情報)を保持する必要があるわけです。

僕が知っている限り、永続的セッション情報の保存方法には主に2つあります。

A. Rails側でCookiesにトークンを設定する(Devise gemの標準動作)
B. Railsでレスポンス(ヘッダやJSONなど)の中にトークンを書いて、フロントエンド側でどうにかしてトークンを保存する(Devise_token_auth gem)

この「トークン」は、クライアント(フロントエンド)からのリクエスト毎に送信して、Railsではデータベースのトークンと一致するかどうかを確認して、ユーザーを識別します。

どちらの方法でも、Rails側でトークンの検証をすることには変わりないので、二つの方法の間で異なるのは、トークンの受け渡し方法と保存方法だけです。

想定環境

今記事で想定しているのは、フロントエンドとバックエンドが分離している環境です。
なので、Railsでフロントエンド&バックエンド処理が完結している環境は対象外です。
(そのような環境なら、方法Bを使わずに素直に方法Aを使うべきでしょう)

具体的には、フロントエンドNext.js(React.js)・バックエンドRails APIです。
フロントエンドはWebアプリでなくてもネイティブアプリなど何でもいいです。
Next.jsというフレームワークを用いていますが、今記事ではNext.js特有の機能には触れてないので、Next.js≒React.jsと解釈していただいて構いません。

CrafterePostのサーバー構成

結論

  • フロントエンドがWebアプリのみなら方法A
  • フロントエンドにネイティブアプリがあるなら方法B
  • フロントエンドがWebアプリのみでDeviseを使うなら、devise_token_auth導入の手間が省ける方法Aのほうがいい
  • セキュリティ強度はあまり変わらない

トークンの保存方法

方法Aの場合、Cookieに保存されます。

ネイティブアプリによってはCookieに対応していない場合があるので、ネイティブアプリの対応を考えるのなら方法Bを用います。

方法Bの場合、「フロントエンド側でどうにかしてトークンを保存」すればいいので、様々な方法が考えられますが、主な方法として3つ考えられるでしょう。

  1. (Webアプリの場合)Local Storageに保存
  2. (Webアプリの場合)JavaScriptでCookieに保存
  3. (ネイティブアプリの場合)セキュアなキーストアを用いる(Android Keystoreなど)

保存方法3の場合はセキュリティ的に大丈夫ですが、問題になるのは保存方法1と2です。

XSS攻撃に対する脆弱性

XSS攻撃とは、攻撃者が悪意のあるコードをWebページに埋め込み、被害者がページを開くとそのコードが実行されて機密情報が盗まれる攻撃です。

最近のReact.jsなどのフレームワークは、HTMLタグをエスケープ処理して無害なテキストに変換して表示する機能が標準で備わっていますが、そういった外部スクリプト自体に脆弱性がある場合も無きにしも非ずです。

Local StorageやJavaScriptによって設定されたCookieはXSS攻撃に対して脆弱です。
XSS攻撃をされたら簡単に盗めてしまいます。

具体的な手順

方法Bの場合、自動的にCookieに保存してはくれないので、トークンを取得したらJavaScriptで格納する必要があります。

ですが、これらのデータは当然ながらJavaScriptで自由に読み書きできるので、不正なコードで簡単に盗めてしまいます。

方法Aの場合は?

「Cookieが盗まれるなら、方法AのCookieだって盗まれるじゃないか!!」
勘が良いですね。

ここで、方法AのCookieと方法BのCookieを比較してみましょう。
(上が方法B、下が方法Aです)

右端にご注目ください。
なんと、方法AのCookieにはHttpOnly属性がついています!!

HttpOnly属性がついているCookieはJavaScriptで読み書きすることができません!!
この属性を付与することによって、XSS攻撃によるCookie盗取を防ぐことができます。
(リクエスト送信時には、HttpOnly属性がついているCookieも一緒にサーバーに送信できるので、Rails側のみトークンを確認できます)

注意
RailsでHttpOnly属性つきのCookieを作成するには、明示的に指定する必要があります。
Deviseは自動でHttpOnly属性を付与するようです(要確認)
cookies.permanent[:access_token] = { value: "認証トークン", httponly: true }

HttpOnly属性の効果

「HttpOnly属性最強!!HttpOnly属性しか勝たん!!」みたいな雰囲気が出てきましたが、そうでもないみたいです。

Cookieを直接読み取ることができなくても、Railsサーバーから返却されるレスポンスデータを読むことはできます。

また、HttpOnly属性がついていてJavaScriptで読み取れなくても、Cookieを同時送信することはできるので、ユーザーのブラウザに読み込まれた悪意のあるコードから不正リクエストを送信することはできます。

このようにCookieを直接取得できなくても攻撃方法は沢山あるので、HttpOnly属性の効果はあまりないのかもしれません…

参考動画

結論

今記事では、XSS攻撃に焦点を当てて認証トークン保存方法の検討を行いました。

結論として、どの保存方法を用いてもさほどセキュリティ強度は変わらないことがわかりました。
その他の保存方法として、Auth0というのがあるらしいですが、認証トークンが盗まれなくても攻撃の仕方はいろいろあるので、結局一緒なのかなーって思います。

こればかりはXSS攻撃対策を頑張るしかありません….

コメント

タイトルとURLをコピーしました