naichi's lab

3日後の自分(他人)への書き置き

【Rails】ローテータを使用した場合にpermanent cookieの有効期限がSessionに変わってしまう

バージョン

> ruby -v     
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
> rails -v    
Rails 7.0.4.3

先にまとめ

結論

signedかつpermanentなcookieを使う場合は、cookie書き込み箇所だけでなく読取箇所にもpermanentを書いておいた方が安心。

# 書き込み箇所
cookies.permanent.signed[:user_id] = user.id

# 読み取り箇所
user_id = cookies.permanent.signed[:user_id]

理由

  • cookieローテータを使用した場合には読取箇所でもcookieの再作成&保存が行われる。
  • もしpermanentが書かれていないと 有効期限=Session となり意図しないcookieが保存されてしまう。

ここからは詳細

つい先日 unityroom を、Rails6からRails7にアップデートしました。

Railsのアップグレードガイド に従い、signed cookieのローテータも設定しました。

unityroomではログインを永続化(正確には自動再ログイン)するために user_idremember_token というcookieをセットしています。

  def remember(user)
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember
  end

2つのcookieはpermanent指定によって長期間の有効期限が設定されている

Rails6でログインし、Rails7に上げてログインが維持されていることを確認。
よしよしローテータも正しく動いているな。とリリースしました。

不具合報告

その後、数日がたち、数名の方からunityroomからログアウトされてたんですけど何かしました?っていう連絡が。

え、こわ。なんだろうと調査を開始。

しばらくして再現できる手順を見つけました。

  1. Rails6でサーバーを起動し、ログイン
    1. 先ほど書いた通りuser_idとremember_tokenがセットされる
  2. ブラウザを完全に閉じて有効期限がSessionとなっているクッキーを消す
  3. Rails7にアップデート&サーバー再起動
  4. ブラウザを開いて再度アクセス

この時点でのcookieは・・・

user_idの有効期限がSessionになってる。なんで!

何故かuser_idの有効期限がSessionになっており、当然この状態でブラウザを閉じるとuser_idは消えてしまいます。

自動ログインはuser_idとremember_tokenを組み合わせて認証していたので、ログアウトされたかのような挙動になっていたのでした。

原因調査

ではなぜ意図せず有効期限がSessionになるのか?

このcookieを書き込んでいる箇所はログイン時の1箇所。

  def remember(user)
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember
  end

コードが高度すぎて半分も理解できないRailsのコードを追いかけてみたら、cookies.signedを呼び出すとSignedKeyRotatingCookieJarがnewされており、さらにSignedKeyRotatingCookieJarのコンストラクタでローテータが実行されてそうな記述を見つけました。

んー、これってもしかしてcookie読取時にもローテータが動く?

user_id cookieを読み込んでいる箇所は1箇所

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

user_id = cookies.signed[:user_id]session[:user_id] が有効な場合は通らないので、再現手順とも合致します。

まさかここでローテータが働き、permanentがつかない(=Session)Cookieが保存されている??

まさかと思いながらも以下のように書き換えたら正しく有効期限の長いcookieが保存されました。

elsif (user_id = cookies.permanent.signed[:user_id]) 

えぇ...

  • ローテータって読み込んだCookie自体の有効期限をうまいこと引き継いで再保存してくれるわけじゃないの?
  • クッキー読み込む箇所でクッキー保存しちゃうの?

など色々思いましたが、
まあ確かに普段読取しかしないようなクッキーもローテートしてとかないと、
いつローテータ外していいかわかんないしこの挙動で正解なんだろうなと納得しました。

対策

ローテータを使う機会はそんなに頻繁にあるわけじゃないですが、 cookies.permanent.signed[:hoge] = 123 みたいなクッキーを書き込む場合は、
読み込む箇所にも permanentをつけて hoge = cookies.permanent.signed[:hoge] というふうに書いておくと安心ですね。というお話でした。

あ〜〜疲れた。