web-dev-qa-db-ja.com

Rails、デバイス認証、CSRFの問題

私はRailsを使用して単一ページのアプリケーションを実行しています。サインインおよびサインアウトすると、Adevを使用してDeviseコントローラーが呼び出されます。私が得ている問題は、1)サインイン2)サインアウトしてから再度サインインしても機能しないことです。

私はサインアウトするとリセットされるCSRFトークンに関連していると思います(それは気づくべきではありませんが)、単一ページなので、古いCSRFトークンがxhrリクエストで送信され、セッションがリセットされます。

より具体的には、これがワークフローです。

  1. サインイン
  2. サインアウト
  3. サインイン(201に成功。ただし、WARNING: Can't verify CSRF token authenticityサーバーログ内)
  4. 後続のAjaxリクエストが失敗する401未承認
  5. Webサイトを更新します(この時点で、ページヘッダーのCSRFが別のものに変更されます)
  6. サインアウトできますが、サインアウトして再度サインインするまで機能します。

どんな手がかりも大歓迎です!詳細を追加できるかどうかを教えてください。

39
vrepsys

神保は、あなたが直面している問題の背後にある「なぜ」を説明する素晴らしい仕事をしました。この問題を解決するには、次の2つの方法があります。

  1. (Jimbo推奨)Devise :: SessionsControllerをオーバーライドして、新しいcsrf-tokenを返します。

    class SessionsController < Devise::SessionsController
      def destroy # Assumes only JSON requests
        signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
        render :json => {
            'csrfParam' => request_forgery_protection_token,
            'csrfToken' => form_authenticity_token
        }
      end
    end
    

    そして、クライアント側でsign_outリクエストの成功ハンドラーを作成します(セットアップに基づいていくつかの調整が必要になる可能性があります(例:GET vs DELETE)):

    signOut: function() {
      var params = {
        dataType: "json",
        type: "GET",
        url: this.urlRoot + "/sign_out.json"
      };
      var self = this;
      return $.ajax(params).done(function(data) {
        self.set("csrf-token", data.csrfToken);
        self.unset("user");
      });
    }
    

    また、これは、すべてのAJAXリクエストに次のようなものが含まれるCSRFトークンを自動的に含めることを前提としています。

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. さらに簡単に言えば、アプリケーションに適切な場合は、Devise::SessionsControllerをオーバーライドし、skip_before_filter :verify_authenticity_tokenでトークンチェックをオーバーライドできます。

38
jredburn

私もこの問題に遭遇しました。ここでは多くのことが行われています。

TL; DR-失敗の理由は、CSRFトークンがサーバーセッションに関連付けられていることです(ログインしているかログアウトしているかにかかわらず、サーバーセッションがあります)。 CSRFトークンは、ページが読み込まれるたびにページのDOMに含まれます。ログアウトすると、セッションはリセットされ、csrfトークンはありません。通常、ログアウトは別のページ/アクションにリダイレクトされ、新しいCSRFトークンが提供されますが、ajaxを使用しているため、これを手動で行う必要があります。

  • Devise SessionController :: destroyメソッドをオーバーライドして、新しいCSRFトークンを返す必要があります。
  • 次に、クライアント側で、ログアウトXMLHttpRequestの成功ハンドラーを設定する必要があります。そのハンドラーでは、応答からこの新しいCSRFトークンを取得し、domに設定する必要があります:$('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

詳細な説明 ApplicationController.rbファイルに_protect_from_forgery_が設定されている可能性が高く、そこから他のすべてのコントローラーが継承されます(これはかなり一般的だと思います)。 _protect_from_forgery_は、すべての非GET HTML/Javascript要求に対してCSRFチェックを実行します。 Devise LoginはPOSTであるため、CSRFチェックを実行します。 CSRFチェックが失敗した場合、サーバーは攻撃(正しい/望ましい動作)であると想定するため、ユーザーの現在のセッションはクリアされます。つまり、ユーザーがログアウトされます。

したがって、ログアウト状態で開始すると仮定すると、新しいページのロードを行い、ページを再度リロードすることはありません。

  1. ページのレンダリング時:サーバーは、サーバーセッションに関連付けられたCSRFトークンをページに挿入します。このトークンを表示するには、browser$('meta[name="csrf-token"]').attr('content')のJavaScriptコンソールから次を実行します。

  2. その後、XMLHttpRequestを介してサインインしますこの時点でCSRFトークンは変更されないため、セッションのCSRFトークンはページに挿入されたものと一致します。舞台裏では、クライアント側で、jquery-ujsはxhrをリッスンし、自動的に$('meta[name="csrf-token"]').attr('content')の値を持つ 'X-CSRF-Token'ヘッダーを設定します(これは、CSRFトークンがサーバーによるステップ1)。サーバーは、jquery-ujsによってヘッダーに設定されたトークンと、セッション情報に保存されているトークンを比較し、一致するように要求が成功します。

  3. その後、XMLHttpRequestを介してログアウトします:これはセッションをリセットし、CSRFトークンのない新しいセッションを提供します。

  4. その後、XMLHttpRequestを介して再度サインインします jquery-ujsは、$('meta[name="csrf-token"]').attr('content')の値からCSRFトークンを取得します。この値はまだ[〜#〜] old [〜#〜] CSRFトークンです。この古いトークンを受け取り、それを使用して「X-CSRF-Token」を設定します。サーバーは、このヘッダー値を、セッションに追加する新しいCSRFトークンと比較しますが、これは異なります。この違いにより_protect_form_forgery_が失敗し、_WARNING: Can't verify CSRF token authenticity_がスローされ、セッションがリセットされてユーザーがログアウトされます。

  5. ログインしたユーザーを必要とする別のXMLHttpRequestを作成します:現在のセッションにはログインしたユーザーがいないため、deviseは401を返します。

更新:8/14 Devise logoutは新しいCSRFトークンを提供しません。ログアウト後に通常発生するリダイレクトは、新しいcsrfトークンを提供します。

32
plainjimbo

私の答えは@Jimboと@Sijaの両方から大きく借りていますが、 Rails CSRF Protection + Angular.js:protect_from_forgeryによりPOSTでログアウトする で提案されたdevise/angularjsの規則を使用しています blog で最初にこれをやったとき。これには、csrfのcookieを設定するためのアプリケーションコントローラー上のメソッドがあります。

after_filter  :set_csrf_cookie_for_ng

def set_csrf_cookie_for_ng
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end

だから私は@Sijaのフォーマットを使用していますが、以前のSOソリューションからのコードを使用して、私に与えます:

class SessionsController < Devise::SessionsController
  after_filter :set_csrf_headers, only: [:create, :destroy]

  protected
  def set_csrf_headers
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?  
  end
end

完全を期すために、作業に数分かかったため、config/routes.rbを変更して、セッションコントローラーをオーバーライドしたことを宣言する必要があることにも注意してください。何かのようなもの:

devise_for :users, :controllers => {sessions: 'sessions'}

これは、アプリケーションで行った大規模なCSRFクリーンアップの一部でもあり、他の人にとっては興味深いかもしれません。 ブログの投稿はこちら 、その他の変更点は次のとおりです。

ActionController :: InvalidAuthenticityTokenからのレスキュー。つまり、同期が取れなくなった場合、ユーザーがCookieをクリアする必要はなく、アプリケーション自体が修正されます。 Railsに立つと、アプリケーションコントローラーはデフォルトで次のようになります:

protect_from_forgery with: :exception

その場合、必要なものは次のとおりです。

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render :error => 'invalid token', {:status => :unprocessable_entity}
end

また、競合状態にいくつかの悲しみとDeviseのタイムアウト可能モジュールとのやり取りがありました。これについてはブログ投稿でさらにコメントしました。要するに、cookie_storeではなくactive_record_storeを使用することを検討し、並列発行に注意してくださいsign_inおよびsign_outアクションの近くのリクエスト。

9
PaulL

これは私の意見です:

class SessionsController < Devise::SessionsController
  after_filter :set_csrf_headers, only: [:create, :destroy]
  respond_to :json

  protected
  def set_csrf_headers
    if request.xhr?
      response.headers['X-CSRF-Param'] = request_forgery_protection_token
      response.headers['X-CSRF-Token'] = form_authenticity_token
    end
  end
end

そして、クライアント側で:

$(document).ajaxComplete(function(event, xhr, settings) {
  var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
  var csrf_token = xhr.getResponseHeader('X-CSRF-Token');

  if (csrf_param) {
    $('meta[name="csrf-param"]').attr('content', csrf_param);
  }
  if (csrf_token) {
    $('meta[name="csrf-token"]').attr('content', csrf_token);
  }
});

CSRFメタタグは、X-CSRF-TokenまたはX-CSRF-Param ajaxリクエスト経由のヘッダー。

8
Sija

ワーデンのソースを掘り下げた後、sign_out_all_scopesからfalseは、Wardenがセッション全体をクリアするのを停止するため、サインアウトの間、CSRFトークンは保持されます。

Devise issue tackerの関連ディスカッション: https://github.com/plataformatec/devise/issues/22

6
Lucas

これをレイアウトファイルに追加したところ、うまくいきました

    <%= csrf_meta_tag %>

    <%= javascript_tag do %>
      jQuery(document).ajaxSend(function(e, xhr, options) {
       var token = jQuery("meta[name='csrf-token']").attr("content");
        xhr.setRequestHeader("X-CSRF-Token", token);
      });
    <% end %>
1
r15

Application.jsファイルにこれが含まれているかどうかを確認してください

// = jqueryが必要

// = jquery_ujsが必要

理由はjquery-Rails gemであり、デフォルトですべてのAjaxリクエストにCSRFトークンを自動的に設定し、これら2つが必要です

0
pdpMathi

私の場合、ユーザーをログインした後、ユーザーのメニューを再描画する必要がありました。それはうまくいきましたが、同じセクションでサーバーへのすべてのリクエストでCSRF認証エラーが発生しました(もちろんページを更新せずに)。上記のソリューションは、jsビューをレンダリングする必要があるため機能しませんでした。

Deviseを使用してこれを行いました:

app/controllers/sessions_controller.rb

   class SessionsController < Devise::SessionsController
      respond_to :json

      # GET /resource/sign_in
      def new
        self.resource = resource_class.new(sign_in_params)
        clean_up_passwords(resource)
        yield resource if block_given?
        if request.format.json?
          markup = render_to_string :template => "devise/sessions/popup_login", :layout => false
          render :json => { :data => markup }.to_json
        else
          respond_with(resource, serialize_options(resource))
        end
      end

      # POST /resource/sign_in
      def create
        if request.format.json?
          self.resource = warden.authenticate(auth_options)
          if resource.nil?
            return render json: {status: 'error', message: 'invalid username or password'}
          end
          sign_in(resource_name, resource)
          render json: {status: 'success', message: '¡User authenticated!'}
        else
          self.resource = warden.authenticate!(auth_options)
          set_flash_message(:notice, :signed_in)
          sign_in(resource_name, resource)
          yield resource if block_given?
          respond_with resource, location: after_sign_in_path_for(resource)
        end
      end

    end

その後、メニューを再描画するcontroller#actionにリクエストを行いました。そして、javascriptで、X-CSRF-ParamとX-CSRF-Tokenを変更しました:

app/views/utilities/redraw_user_menu.js.erb

  $('.js-user-menu').html('');
  $('.js-user-menu').append('<%= escape_javascript(render partial: 'shared/user_name_and_icon') %>');
  $('meta[name="csrf-param"]').attr('content', '<%= request_forgery_protection_token.to_s %>');
  $('meta[name="csrf-token"]').attr('content', '<%= form_authenticity_token %>');

同じjs状況にいる人に役立つことを願っています:)

0
JGutierrezC

私の状況はさらに簡単でした。私の場合、私がしたかったのはこれだけです:ユーザーがフォームのある画面に座っていて、セッションがタイムアウトした場合(タイムアウト可能なセッションタイムアウトを回避する)、通常、その時点でSubmitを押すと、Deviseはそれらをバウンスしますログイン画面に。まあ、私はそれを望んでいませんでした、なぜなら彼らはすべてのフォームデータを失うからです。 JavaScriptを使用してフォームの送信をキャッチし、Ajaxがコントローラーを呼び出してユーザーがサインインしていないかどうかを判断します。その場合は、パスワードを再入力するフォームを作成し、コントローラーで再認証します(bypass_sign_in) Ajax呼び出しを使用します。その後、元のフォームの送信を続行できます。

Protect_from_forgeryを追加するまで完璧に機能していました。

したがって、上記の回答のおかげで、本当に必要なのは、ユーザーに再度サインインする(bypass_sign_in)コントローラーで、新しいCSRFトークンにインスタンス変数を設定するだけでした。

@new_csrf_token = form_authenticity_token

そして、レンダリングされた.js.erbで(これもXHR呼び出しだったため):

$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');

出来上がり。更新されなかったため、古いトークンで固定されていたフォームページには、ユーザーのサインインから取得した新しいセッションからの新しいトークンがあります。

0
Jason Perrone