Search code examples
ruby-on-railsrefactoringauthenticity-token

InvalidAuthenticityToken from form in Rails 5.2


I had a working Rails5.2 application that needed a database restructure to improve the organisation of some tables. Since this change, I've found that some of my forms fails consistently on my local machine with this error:

ActionController::InvalidAuthenticityToken in PayBandsController#create

I've had to change a lot in the application as part of the refactor, of course, but the tests are all passing.

I can avoid the issue by setting skip_before_action :verify_authenticity_token on the relevant controller (per previous SO threads) but this isn't a good fix, of course. I'd like to know what the root cause is and eliminate it.

  • The form works when it's on its own page, placed with render in the HTML.
  • The form works when I set the skip_before_action.
  • The form fails with an AuthenticityToken error otherwise - even though the same structure worked before.

This is all tested on my local machine with a valid session.


The form is created within a service object as the final line of a data table (so that I can add new lines to the table). Is this a potential cause of the problem - it's rendered in the service object rather than in the ERB? Although this approach worked fine before the refactor...

_table_data += ApplicationController.render(partial: 'datasets/pay_band_numbers_form', locals:{ _dataset_id: _dataset.id, _namespace: _key })

The partial is as follows and, as I said, works if placed on a page by itself (e.g. pay_bands/new) but not where it needs to be (on dataset/edit):

<%= form_for (PayBand.new), namespace: _namespace do |f| %>
  <%= f.hidden_field :dataset_id, :value => _dataset_id %>
  <%= f.text_field :label, :tabindex => 11, :required => true %>
  <%= f.number_field :number_women, :tabindex => 12, :required => true %>
  <%= f.number_field :number_men, :tabindex => 13, :required => true %>
  <%= f.submit "&#10004;".html_safe, :tabindex => 14, class: "button green-button checkmark" %>
<% end %>

The models involved are Dataset and PayBand, which are defined as follows.

class Dataset < ApplicationRecord
  belongs_to :organisation
  has_many :pay_bands, inverse_of: :dataset, dependent: :destroy
  accepts_nested_attributes_for :pay_bands
  default_scope -> { order(created_at: :desc) }
  validates :name, presence: true
  validates :organisation_id, presence: true
end
class PayBand < ApplicationRecord
  belongs_to :dataset
  has_many :ages_salaries_genders, inverse_of: :pay_band, dependent: :destroy
  validates :dataset_id, presence: true
  validates :label, presence: true
end

The relevant parts of the dataset controller are:

class DatasetsController < ApplicationController
  protect_from_forgery with: :exception
  load_and_authorize_resource
  before_action :authenticate_user!
.
.
.
  def edit
    @dataset = Dataset.find(params[:id])
  end

  def update
    @dataset = Dataset.find(params[:id])
    if @dataset.update_attributes(dataset_params) && dataset_params[:name]
      flash[:notice] = "Survey updated"
      redirect_back fallback_location: dataset_path(@dataset)
    else
      flash[:alert] = "Attempted update failed"
      redirect_to edit_dataset_path(@dataset)
    end
  end
.
.
.
  private

    def dataset_params
      params.require(:dataset).permit(:name)
    end

end

The pay-band controller is:

class PayBandsController < ApplicationController
  protect_from_forgery with: :exception
  # skip_before_action :verify_authenticity_token #DANGEROUS!!!!!
  load_and_authorize_resource
  before_action :authenticate_user!

  def create
    @pay_band = PayBand.new(pay_band_params)
    if @pay_band.save
      flash[:notice] = "Data saved!"
      redirect_to edit_dataset_path(Dataset.find(@pay_band.dataset_id))
    else
      flash[:alert] = "There was an error. Your data was not saved."
      redirect_back fallback_location: edit_dataset_path(Dataset.find(pay_band_params[:dataset_id]))
    end
  end

  private

    def pay_band_params
      params.require(:pay_band).permit(:label, :number_women, :number_men, :avg_salary_women, :avg_salary_men, :dataset_id)
    end
end

All of the above controller structure is identical to the working version of the app, before I fiddled with the data models!

===

Edit

I decided to check the headers, after some more research, so see if I could see the token value. However, the token value in the meta csrf-token header tag doesn't match any of the 3 different tokens in the 3 different forms on this page. But one works anyway (the simple form that's just rendered in the ERB template) and the other two don't (the ones rendered in the service object). Which all leaves me just as confused as I was before!

Also, I tried removing Turbolinks from my GemFile and application.js file, re-running bundle and rails s, but it made no difference.


Solution

  • For future reference for anyone else having the same problem (AuthenticityToken errors for a form that's been built in a helper or service object), I moved the form directly into the view and rendered it there - and the problem disappeared. I've removed the skip_before_action :verify_authenticity_token and all is now well!