The Invalid Authenticity Token Experiment

August 19, 2017

I like experimenting. My most recent experiment was to make this site use SSL throughout. Given that it is currently just a blog, https isn’t strictly necessary. But that isn’t the point. I had already tried Amazon’s Route 53 for DNS and now I wanted to play with something else.

I decided to give Cloudflare a shot. I heard about it from a friend and wanted to see what it was all about. As one might expect, the new technology created a few bumps in the road. Most notably, form posts were taunting me with 422 - Unprocessable Entity errors. Here is how I solved it.

Ingredients

  • 64bit Amazon Linux 2017.03 v2.4.2 running Ruby 2.3 (Puma)
  • Nginx 1.10.3
  • Rails 5.1.2
  • Cloudflare with flexible SSL

Problem

The switch to Cloudflare was quick and easy. So easy in fact, that I couldn’t resist turning on SSL everywhere while I was there. It was a simple switch. I didn’t have to request a certificate or do anything complicated at all. Nice.

The site worked just as it had before except now I had a nice green padlock in my browser. Life was good . . . until I tried to post the next article.

I use AJAX form submissions (via the Rails remote option) and for the first time, the POST request was answered with a 422 - Unprocessable Entity error. The Rails production log had this to say:

HTTP Origin header (https://ramekintech.com) didn't match request.base_url (http://ramekintech.com)
Completed 422 Unprocessable Entity in 1ms
ActionController::InvalidAuthenticityToken

The difference was the addition of SSL. A simple letter ‘s’ in the origin header broke form submission on my site. Bummer.

Solution

The solution was all about the Nginx configuration. I learned from this rails issue that there were a few additional headers that needed to be put into my nginx.conf file. Until now, I had been using the configuration that came with the default AWS Elastic Beanstalk installation. Here are the changes I had to make:

http {
  ...
  upstream myapp {
    server unix:/var/run/puma/my_app.sock;
  }
  ...
  server {
  	...
  	server_name  ramekintech.com;
  	...
    location / {
      proxy_pass        http://myapp;
      proxy_set_header  X-Forwarded-Ssl on;
      proxy_set_header  X-Forwarded-Host $host;
    }
    ...
  }
  ...
}

Items to note:

  • The location and name of the puma socket file may vary for other installations.
  • myapp and my_app are not placeholders. Everything shown is precisely what solved the problem for me.
  • The server_name was set to localhost and that did not work. Changing it to ramekintech.com finally got it all working.

The Other Solution

The other solution I found to this problem was to turn off CSRF (Cross Site Request Forgery) protection or disable it for routes that it caused a problem for. I do not like that answer at all. I believe Rails gives us this protection by default for a reason. If this is new to you, it is what the protect_from_forgery with: :exception line in the ApplicationController is doing.

I’ve read that the attack isn’t very prevalent. The Rails Security Docs state that “CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures)”. But that doesn’t matter to me. I want to be as secure as possible. Especially when usability is not compromised.

Final Bits

If CSRF protection ever gives you grief, please don’t just disable it for a quick fix. Find out why it is happening and solve the real problem. Every little bit helps to make the web a safer place for everyone. It took some extra effort to get my site working again but I feel it is time well spent.

Here is the full nginx.conf file just in case there are other settings that contributed to my success. The SSL section is not omitted. Because Cloudflare is handling the https traffic, I do not have or need an SSL section here. I know this creates a section in the request chain that is not encrypted. But, like I said, I am experimenting. I’ll be solving that problem with the next experiment.

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    index   index.html index.htm;

    upstream myapp {
      server unix:/var/run/puma/my_app.sock;
    }

    server {
        listen       80 ;
        listen       [::]:80 ;
        server_name  ramekintech.com;
        root         /usr/share/nginx/html;

        include /etc/nginx/default.d/*.conf;

        location / {
          proxy_pass        http://myapp;
          proxy_set_header  X-Forwarded-Ssl on;
          proxy_set_header  X-Forwarded-Host $host;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}