これはとくまるひろしの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
これでようやく前に進めるー!!
