これはとくまるひろしのSession Fixation攻撃入門でも紹介されている通り、セッションハイジャックの可能性を高めてしまうので、本来はオススメではありません。私も違う方法を取りました。
しかし、調べるのが大変だったので、備忘録として残すこととします。
環境は、
- Rails 4.1.4
- Devise
作っていたものは、以下のようなもの。
- 既にアカウントはログイン済みである。
- 他の場所でアカウントのログインが試みられる
- 一旦ログインさせずに、他のユーザーがログインしていることを通知
- それでもログインしたかったら現在ログイン中のユーザーをログアウトさせてログインする
これを実現させるために、ログインしたタイミングでセッションIDを保存しておいて、現在のセッションIDと違う場合は弾くというふうにしようと思い、作っていました。ところが、DeviseというかWardenは、ログインしたタイミングでセッションIDが変わるみたいでした。そのため、保存したセッションIDは既に無効になっていました。
これは、先のリンクにある、Session Fixation Attackへの防御の為のようです。
参考リンク:https://github.com/hassox/warden/issues/94
とりあえずの回避策としては、セッションIDが変わらなければいいので、まずはそれを目指しました。できたコードがこれ。
class User::SessionsController < Devise::SessionsController Warden::Manager.after_authentication do |user, auth, opts| auth.request.session_options[:renew] = false end # 後はcreateとかでログイン処理したタイミングでセッションIDを保存する end
こうすると、ログイン前とログイン後でセッションIDが変わりません。
わかるとたいした事はないのですが、ここに行き着くのに1日費やしてしまいました。そもそもが、あんまり需要のない機能なのかもしれません…。とりあえず解決はしたのですが、これだと先の攻撃をされる可能性があり得るので、違う方法で回避したいと思い、次のようにしました。
最終的な回避策としては、セッションID更新予約フラグをセッションに持たせました。
class User::SessionsController < Devise::SessionsController def create self.resource = warden.authenticate!(auth_options) if resource.other_already_signed_in?(request.session_options[:id]) warden.logout session[:user_id] = resource.id redirect_to user_confirmation_login_path else super do |resource| set_reserve_update_session_id end end end def confirmation_login end def force_login self.resource = User.find session[:user_id] set_flash_message(:notice, :signed_in) if is_flashing_format? sign_in(resource, event: :authentication) set_reserve_update_session_id respond_with resource, location: after_sign_in_path_for(resource) end private def set_reserve_update_session_id session[:reserve_update_session_id] = true end end
これを、application_controller.rbで確認させるようにしました。
強制ログアウトもあるので、prepend_before_actionを使って、ブロックで処理しました。
class ApplicationController < ActionController::Base prepend_before_action do update_session_id check_force_logout unless devise_controller? end private def update_session_id if current_user && session[:reserve_update_session_id] current_user.session_id = request.session_options[:id] current_user.last_access_at = Time.now current_user.save! session.delete :reserve_update_session_id end end def check_force_logout if current_user && current_user.session_id != request.session_options[:id] sign_out current_user redirect_to new_user_session_path, alert: '他の方がログイン中のため、ログアウトされました' end end end
これでようやく前に進めるー!!