😥

devise_token_authとSWRを使う場合の注意点: access-tokenが古くなり認証が通らなくなる

2021/09/13に公開

概要

  • devise_token_authchange_headers_on_each_request オプションを有効( true )にして SWR を使うと、特定の操作でaccess-tokenの更新に失敗し、それ以降のリクエストでユーザ認証に失敗する
    • 特定の操作: 少しブラウザからフォーカスを外した後に、フォーカスして数秒以内に手動でリロード する、など
  • 対処方法は change_headers_on_each_requestfalse にするか、SWRの自動更新を使わない ( useSWRImmutable を使う ) にするかのどちらかになるかと思います。

devise_token_auth と change_headers_on_each_request について

https://devise-token-auth.gitbook.io/devise-token-auth/

Ruby on RailsをAPI Modeで使う時には devise_token_auth が便利です。deviseと同じ操作感で、tokenを使った認証APIを作ることができます。

devise_token_auth に change_headers_on_each_request というオプションがあり、これを有効にすることで、リクエストごとにtokenが更新されるようになります。
これにより最新以外のtokenが何らかの事情で流出しても情報が抜き出されることが無くなります。
デフォルトでは true が設定されています。

しかしリクエストのたびにtokenを更新する場合、SPAやMobile Appでよく発生する「1画面で複数リクエストを同時に送る」といった操作が理論上できなくなってしまい、パフォーマンスが大幅に下がってしまう可能性があります。
それを可能にするために devise_token_auth では 5秒間(デフォルト)は同じtokenでリクエストを受け付ける という機構が入っています。

devise_token_auth: docs/conceptual.md

同時に送られたリクエストだと判定されたリクエスト達のうち、最初の一つだけでtokenが返却され、それ以降のリクエストではaccess-token には ' ' が格納され返却されます。

Note that when the server identifies that a request is part of a batch request, the user's auth token is not updated. The auth token will be updated and returned with the first request in the batch, and the subsequent requests in the batch will not return a token. This is necessary because the order of the responses cannot be guaranteed to the client, and we need to be sure that the client does not receive an outdated token after the the last valid token is returned.

実装: set_user_by_token.rb

SWRによる自動Fetch

https://swr.vercel.app/

Vercelが開発しているAPI callを主な用途としたライブラリです。
公式ドキュメントのトップにもある通り、キャッシュを保持してくれたり、裏側でデータを引っ張ってきてくれて自動で更新してくれたり、エラー時に自動的にリクエストを送り直したりしてくれます。

再取得するタイミングは、ページのフォーカス時、refreshIntervalごと、オフラインからオンラインになった時があります。

起きる問題

上記で説明した通り、SWRは特定条件で自動的にAPIにアクセスしに行きデータを更新しようとします。

その自動更新と、ユーザの手動でのデータ更新がかぶることで、devise_token_auth側でrace condition (=複数同時リクエストによる競合)だと判定され、上で説明した通り、複数のリクエストのうち一つだけにしかaccess-tokenが返却されなくなります。

その受け取ったtokenをcookieにちゃんと保管できれば良いのですが、SWRのリクエストの後にユーザによるリロードやページ遷移が発生した場合、遷移前で実行されたSWRで受け取るtokenは破棄されてしまいます。
そうなった場合、古いaccess-tokenがcookieに保持され続けるか、空の値が保存されてしまい、それ以降のユーザ認証が全てUnauthorizedになりログアウト・ログインのし直しが必要となります。

動きを図解するとこんな感じです。縦が時間軸で、左から右への矢印がAPIへのリクエストです。

対処方法

1. change_headers_on_each_request: false にする

手っ取り早く解決するならこちら。
false にすることで、リクエストごとにtokenが更新されなくなります。
tokenの有効期限は token_lifespan というオプションで設定できます。その期間ごとに再ログインを要求する(かどこかにパスワードを保持して自動ログインする)必要があります。

なお、usersテーブルの tokens を消去することでtokenを強制的にexpireにできます。(これは change_headers_on_each_request: true でも同様)

2. useSWRImmutable を使う

immutableであることが前提の useSWRImmutable が用意されています。
stale-while-revalidateを捨てることになるためSWRの利点が大きく削がれますが、今回の問題は解決することができます。

リロードではなく一つの画面で複数のリクエストを送る処理を書く場合は、 access-token が空でないものだけをcookieに保存する処理を書かないといけないので気をつけてください。

追記
  • 「ページにアクセスした直後に離脱」した場合など、Rails側で処理を行った後に、ブラウザが受け取ってtokenwを更新できなかった場合などはImmutableであっても同事象が発生してしまいます。
  • そのため、2. の方法は使えなさそうです。
  • かなしい。

感想

むずかし〜

Discussion