Rails, проверка подлинности, проблема CSRF

Я использую одностраничное приложение, использующее Rails. При входе в систему и выходе из нее контроллеры Devise вызывается с помощью ajax. Проблема, которую я получаю, заключается в том, что когда я 1) подписываюсь в 2) выходим, тогда вход в систему снова не работает.

Я думаю, что это связано с токеном CSRF, который получает reset, когда я выхожу (хотя это не должно быть afaik), и поскольку это одна страница, старый токен CSRF отправляется в xhr-запросе, таким образом, сброс сеанса.

Чтобы быть более конкретным, это рабочий процесс:

  • Войти
  • Выйти
  • Войдите (успешно 201. Однако печатает WARNING: Can't verify CSRF token authenticity в журналах сервера)
  • Последующий запрос ajax сбой 401 неавторизованный
  • Обновите сайт (на данный момент CSRF в заголовке страницы изменяется на что-то еще)
  • Я могу войти, он работает, пока я не попытаюсь выйти и снова.

Любые подсказки очень ценятся! Дайте мне знать, если я могу добавить более подробную информацию.

Ответ 1

Jimbo сделал потрясающую работу, объясняющую "почему" за проблемой, с которой вы столкнулись. Есть два подхода, которые вы можете предпринять для решения проблемы:

  • (Как рекомендовано Jimbo) Override Devise:: SessionController для возврата нового токена csrf:

    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");
      });
    }
    

    Это также предполагает, что вы включаете токен CSRF автоматически со всеми запросами AJAX с чем-то вроде этого:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  • Гораздо проще, если это подходит для вашего приложения, вы можете просто переопределить Devise::SessionsController и переопределить проверку маркера с помощью skip_before_filter :verify_authenticity_token.

Ответ 2

Я тоже столкнулся с этой проблемой. Здесь многое происходит.

TL; DR. Причина отказа заключается в том, что токен CSRF связан с вашим сеансом сервера (у вас есть сеанс сервера, независимо от того, вошли ли вы в систему или вышли из системы). Маркер CSRF включен на страницу DOM на каждой странице загрузки. При выходе из системы ваша сессия reset и не имеет токена csrf. Как правило, выход из системы перенаправляется на другую страницу/действие, что дает вам новый токен CSRF, но поскольку вы используете ajax, вам нужно сделать это вручную.

  • Вам необходимо переопределить метод Devise SessionController:: destroy, чтобы вернуть новый токен CSRF.
  • Затем на стороне клиента вам нужно установить обработчик успеха для вашего выхода XMLHttpRequest. В этом обработчике вам нужно взять этот новый токен CSRF из ответа и установить его в своем dom: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

Подробное пояснение Скорее всего, вы получили protect_from_forgery в вашем файле ApplicationController.rb, из которого наследуются все ваши другие контроллеры (это довольно часто, как мне кажется). protect_from_forgery выполняет проверки CSRF для всех запросов, не связанных с GET HTML/Javascript. Поскольку Devise Login является POST, он выполняет проверку CSRF. Если CSRF Check завершается с ошибкой, то текущий сеанс пользователя очищается, то есть регистрирует пользователя, потому что сервер принимает на себя атаку (что является правильным/желаемым поведением).

Итак, предполагая, что вы начинаете в состоянии выхода из системы, вы загружаете новую страницу и никогда не перезагружаете страницу:

  • При рендеринге страницы: сервер вставляет в страницу токен CSRF, связанный с вашим сеансом сервера. Вы можете просмотреть этот токен, выполнив следующее из консоли javascript в своем браузере $('meta[name="csrf-token"]').attr('content').

  • Затем вы регистрируетесь через XMLHttpRequest: Ваш токен CSRF остается неизменным в этот момент, поэтому токен CSRF на вашем сеансе по-прежнему совпадает с тем, который был вставлен на страницу. За кулисами, на стороне клиента, jquery-ujs прослушивает xhr и устанавливает заголовок "X-CSRF-Token" со значением $('meta[name="csrf-token"]').attr('content') для вас автоматически (помните, что это был токен CSRF, установленный на шаге 1, с помощью разъединить). Сервер сравнивает набор токенов в заголовке с помощью jquery-ujs и тот, который хранится в вашей информации о сеансе, и они совпадают, чтобы запрос удался.

  • Затем вы выходите через XMLHttpRequest:. Сбрасывает сеанс, дает вам новый сеанс без токена CSRF.

  • Затем вы снова регистрируетесь через XMLHttpRequest: jquery-ujs вытаскивает токен CSRF со значения $('meta[name="csrf-token"]').attr('content'). Это значение по-прежнему является вашим токеном OLD CSRF. Он принимает этот старый токен и использует его для установки "X-CSRF-токена". Сервер сравнивает это значение заголовка с новым токеном CSRF, который он добавляет к вашему сеансу, который отличается. Это различие приводит к сбою protect_form_forgery, который выдает WARNING: Can't verify CSRF token authenticity и сбрасывает ваш сеанс, который регистрирует пользователя.

  • Затем вы создаете другое XMLHttpRequest, для которого требуется зарегистрированный пользователь: В текущем сеансе нет зарегистрированного пользователя, поэтому devolution возвращает 401.

Обновление: 8/14 Выйти из системы не дает вам новый токен CSRF, перенаправление, которое обычно происходит после выхода из системы, дает вам новый токен csrf.

Ответ 3

Мой ответ сильно зависит от обоих @Jimbo и @Sija, однако я использую соглашение о разработке/углов, предложенное в Rails CSRF Protection + Angular.js: protect_from_forgery заставляет меня чтобы выйти из POST, и немного разработал мой блог, когда я изначально это сделал. У этого есть метод на контроллере приложения для установки файлов cookie для csrf:

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

Для полноты, так как мне понадобилось пару минут, чтобы разобраться в этом, я также хочу отметить, что вам нужно изменить конфигурацию /routes.rb, чтобы объявить, что вы переопределили контроллер сеансов. Что-то вроде:

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

Это также было частью большой очистки CSRF, которую я сделал в своем приложении, что может быть интересно другим. сообщение в блоге здесь, другие изменения включают в себя:

Спасение от ActionController:: InvalidAuthenticityToken, что означает, что если что-то не синхронизируется, приложение будет исправлять себя, а не пользователю, нуждающемуся в очистке файлов cookie. Поскольку вещи стоят в рельсах, я думаю, что ваш контроллер приложения будет дефолт:

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, о чем я прокомментировал дальше в сообщении в блоге. Короче говоря, вам следует рассмотреть возможность использования active_record_store, а не cookie_store, и быть осторожным о выдаче параллельных запросов рядом с sign_in и sign_out действиями.

Ответ 4

Это мое взятие:

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.

Ответ 5

После того, как рыть на источник Уорден, я заметил, что установка sign_out_all_scopes на false остановки Warden сбрасывались всю сессию, поэтому маркер CSRF сохраняется между выходами знака.

Связанное обсуждение проблемы разработчика Devise: https://github.com/plataformatec/devise/issues/2200

Ответ 6

Я просто добавил это в мой файл макета, и он работал

    <%= 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 %>

Ответ 7

Проверьте, включили ли вы это в файл application.js

//= требуется jquery

//= требуется jquery_ujs

Причиной этого является jquery-rails gem, который по умолчанию автоматически устанавливает токен CSRF для всех запросов Ajax, для этих двух

Ответ 8

В моем случае, после входа в систему пользователя, мне нужно было перерисовать меню пользователя. Это сработало, но я получил ошибки подлинности CSRF при каждом запросе на сервер, в том же разделе (без обновления страницы, конечно). Выше решения не работали, так как мне нужно было отобразить js-представление.

Я сделал это, используя Devise:

приложение/контроллеры/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

После этого я сделал запрос на действие контроллера #, которое перерисовывает меню. И в javascript я модифицировал X-CSRF-Param и X-CSRF-токен:

Приложение/просмотров/утилиты/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:)

Ответ 9

Моя ситуация была еще проще. В моем случае все, что я хотел сделать, это то, что: если человек сидит на экране с формой и время его сеанса истекло (разработайте тайм-аут сеанса), обычно, если он нажимает кнопку Отправить в этот момент, 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 %>');

Вуаля. Моя страница формы, которая не была обновлена и поэтому была привязана к старому токену, теперь содержит новый токен из нового сеанса, который я получил от входа в систему моего пользователя.

Ответ 10

в ответ на комментарий @sixty4bit; если вы столкнулись с этой ошибкой:

Unexpected error while processing request: undefined method each for :authenticity_token:Symbol` 

заменить

response.headers['X-CSRF-Param'] = request_forgery_protection_token

с

response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s