Devise(Warden)でログインしたときにセッションIDを変えさせない方法

これはとくまるひろしのSession Fixation攻撃入門でも紹介されている通り、セッションハイジャックの可能性を高めてしまうので、本来はオススメではありません。私も違う方法を取りました。

しかし、調べるのが大変だったので、備忘録として残すこととします。
環境は、

  • Rails 4.1.4
  • Devise

作っていたものは、以下のようなもの。

  1. 既にアカウントはログイン済みである。
  2. 他の場所でアカウントのログインが試みられる
  3. 一旦ログインさせずに、他のユーザーがログインしていることを通知
  4. それでもログインしたかったら現在ログイン中のユーザーをログアウトさせてログインする

これを実現させるために、ログインしたタイミングでセッション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

これでようやく前に進めるー!!


カテゴリー Ruby, Ruby on Rails | タグ   | パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です