Ruby on Rails : "The change you wanted was rejected" on Form Submission

Viewed 1207

ruby 3.0.1 rails 6.1.2 'devise', '~> 4.7', '>= 4.7.3'

I'm in a very unusual situation. I am migrating a rails installation from one server to another. I believe I am about 95% of the way, having just restored the production database.

However, anything involving a form submission, including user registration and logging in, gives me the error page:

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

The server log gives me something more helpful:

Completed 422 Unprocessable Entity in 2ms (Allocations: 433)
FATAL -- ActionController::InvalidAuthenticityToken

This confuses me. Because I did regenerate the master.key and credentials.yml.enc and made the contents of the master.key available via the RAILS_MASTER_KEY environment variable. This means that the forms have the proper <input type="hidden" name="authenticity_token" value="<removed for stack_overflow>"> included to shield against cross-site scripting attacks.

I don't think it has anything to do with sessions because even user registration is affected by this. I am using Devise for authentication.

But... now I've hit a brick wall. There's nowhere to go from here. Does anyone know what's wrong?

Update 1

Adding skip_before_action :verify_authenticity_token did allow me to skip past the problem. I am not comfortable with that as a solution.

Update 2

I have these meta tags.

<%= csrf_meta_tags %>
<%= csp_meta_tag %>
2 Answers

I'm ashamed to admit it. But nginx was not configured correctly. Once I got to the breaking point with rails, I finally started asking how else this could be happening.

Here's the deal. Internally, rails was using http to hit various endpoints. I had an nginx redirect block that looked like this:

 server {
  listen 80 default_server;

  server_name _;

  return 301 https://$host$request_uri;
}

This meant that every time the internal endpoint was hit on http, it would turn into https with a 301... Which always is a GET. Once I used config.force_ssl = true in production.rb, I just had to redo my ngix config to support it.

This was the working config.

location / {
  proxy_set_header Host $http_host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto https;
  proxy_redirect off;
  proxy_pass http://app:3000;
}

The reason it looked like an auth token issue was because once the request was sent as a GET, it was then an unprocessable entity. Likely the token was nil or something by the time it got there, even though I could see it in the browser inspect. It was just the verb that was wrong.

Your forms aren't sending the X-CSRF-TOKEN header along with their request. This header is a security feature and you're right to feel uncomfortable turning it off. It's part of what prevents someone on evil.com sending a form to yourwebsite.co.

Out of the box rails <%= forms_* %> do this for you so you likely have something custom.

If you're sending these requests from javascript. I do something this like:

let token = document.querySelector("meta[name='csrf-token']").content
fetch(url, {
    method,
    headers: {
        'X-CSRF-Token': auth_token, // <---- this
        'X-Requested-With': 'XMLHttpRequest',
        ...
    },
...

It's also possible you aren't including it in your application.html.erb so the forms cannot find it.

<!DOCTYPE html>
<html>
<head>
  <%= csrf_meta_tags %> <!-- you need this! -->
...
Related