24

For some reason I'm getting an InvalidAuthenticityToken when making post requests to my application when using json or xml. My understanding is that rails should require an authenticity token only for html or js requests, and thus I shouldn't be encountering this error. The only solution I've found thus far is disabling protect_from_forgery for any action I'd like to access through the API, but this isn't ideal for obvious reasons. Thoughts?

    def create
    respond_to do |format|
        format.html
        format.json{
            render :json => Object.create(:user => @current_user, :foo => params[:foo], :bar => params[:bar])
        }
        format.xml{
            render :xml => Object.create(:user => @current_user, :foo => params[:foo], :bar => params[:bar])
        }
    end
end

and this is what I get in the logs whenever I pass a request to the action:

 Processing FooController#create to json (for 127.0.0.1 at 2009-08-07 11:52:33) [POST]
 Parameters: {"foo"=>"1", "api_key"=>"44a895ca30e95a3206f961fcd56011d364dff78e", "bar"=>"202"}

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
  thin (1.2.2) lib/thin/connection.rb:76:in `pre_process'
  thin (1.2.2) lib/thin/connection.rb:74:in `catch'
  thin (1.2.2) lib/thin/connection.rb:74:in `pre_process'
  thin (1.2.2) lib/thin/connection.rb:57:in `process'
  thin (1.2.2) lib/thin/connection.rb:42:in `receive_data'
  eventmachine (0.12.8) lib/eventmachine.rb:242:in `run_machine'
  eventmachine (0.12.8) lib/eventmachine.rb:242:in `run'
  thin (1.2.2) lib/thin/backends/base.rb:57:in `start'
  thin (1.2.2) lib/thin/server.rb:156:in `start'
  thin (1.2.2) lib/thin/controllers/controller.rb:80:in `start'
  thin (1.2.2) lib/thin/runner.rb:174:in `send'
  thin (1.2.2) lib/thin/runner.rb:174:in `run_command'
  thin (1.2.2) lib/thin/runner.rb:140:in `run!'
  thin (1.2.2) bin/thin:6
  /opt/local/bin/thin:19:in `load'
  /opt/local/bin/thin:19
Simone Carletti
  • 168,884
  • 43
  • 353
  • 360
Optimate
  • 412
  • 2
  • 6
  • 11

7 Answers7

29

With protect_from_forgery enabled, Rails requires an authenticity token for any non-GET requests. Rails will automatically include the authenticity token in forms created with the form helpers or links created with the AJAX helpers--so in normal cases, you won't have to think about it.

If you're not using the built-in Rails form or AJAX helpers (maybe you're doing unobstrusive JS or using a JS MVC framework), you'll have to set the token yourself on the client side and send it along with your data when submitting a POST request. You'd put a line like this in the <head> of your layout:

<%= javascript_tag "window._token = '#{form_authenticity_token}'" %>

Then your AJAX function would post the token with your other data (example with jQuery):

$.post(url, {
    id: theId,
    authenticity_token: window._token
});
jvperrin
  • 3,370
  • 1
  • 22
  • 33
andrewle
  • 1,701
  • 13
  • 11
  • 3
    Optimate should accept this answer. By the way, if you use `$.ajax` you need to put the authenticity token in the data field like this: `$.ajax( { data: { authenticity_token: window._token } } )` – Wylliam Judd Nov 22 '16 at 18:29
12

I had a similar situation and the problem was that I was not sending through the right content type headers - I was requesting text/json and I should have been requesting application/json.

I used curl the following to test my application (modify as necessary):

curl -H "Content-Type: application/json" -d '{"person": {"last_name": "Lambie","first_name": "Matthew"}}' -X POST http://localhost:3000/people.json -i

Or you can save the JSON to a local file and call curl like this:

curl -H "Content-Type: application/json" -v -d @person.json -X POST http://localhost:3000/people.json -i

When I changed the content type headers to the right application/json all my troubles went away and I no longer needed to disable forgery protection.

mlambie
  • 7,396
  • 6
  • 33
  • 41
12

This is the same as @user1756254's answer but in Rails 5 you need to use a bit more different syntax:

protect_from_forgery unless: -> { request.format.json? }

Source: http://api.rubyonrails.org/v5.0/classes/ActionController/RequestForgeryProtection.html

Community
  • 1
  • 1
edwardmp
  • 6,208
  • 5
  • 44
  • 75
  • 7
    Is this not a security risk? – Wylliam Judd Nov 22 '16 at 17:53
  • 1
    @WylliamJudd no, disabling it for only JSON does not pose a security risk as forgery protection goal is to protect HTML forms submitted from other sites – edwardmp Nov 22 '16 at 19:11
  • 2
    When the official documentation says: _'if you're building an API you should change forgery protection method in ApplicationController (by default: :exception)'_, then it's safe enough – Evgeniya Manolova Aug 30 '17 at 23:36
  • this looks like a really bad idea, if your cors allow request from other domains or an attacker has js on one of the allowed domains then the attacker can send CSRF requests. summary: **please don't do this people**, your requests should either pass a custom token to authentify the user, or grab the csrf token from the meta tag – localhostdotdev Apr 14 '19 at 10:07
  • The docs are a bit careless here. "We may want to disable CSRF protection for APIs since they are typically designed to be state-less. That is, the request API client will handle the session for you instead of Rails." So you can disable this if you authenticate your API requests in some other way but DO NOT do this to, for example, allow an authenticated request from a JS snippet in your app. See my response. – thisismydesign Jan 26 '21 at 14:13
3

Adding up to andymism's answer you can use this to apply the default inclusion of the TOKEN in every POST request:

$(document).ajaxSend(function(event, request, settings) {
    if ( settings.type == 'POST' ||  settings.type == 'post') {
        settings.data = (settings.data ? settings.data + "&" : "")
            + "authenticity_token=" + encodeURIComponent( window._token );
    }
});
sth
  • 211,504
  • 50
  • 270
  • 362
Elad Meidar
  • 176
  • 1
  • 5
3

Since Rails 4.2, we have another way is to avoid verify_authenticity_token using skip_before_filter in your Rails App:

skip_before_action :verify_authenticity_token, only: [:action1, :action2]

This will let curl to do its job.

Ruby on Rails 4.2 Release Notes: https://guiarails.com.br/4_2_release_notes.html

Fernando Kosh
  • 3,234
  • 1
  • 33
  • 27
2

As long as the JavaScript lives on the website served by Rails (for example: a JS snippet; or React app managed via webpacker) you can use the the value in csrf_meta_tags included in application.html.erb by default.

In application.html.erb:

<html>
  <head>
    ...
    <%= csrf_meta_tags %>
    ...

Therefore in the HTML of your website:

<html>
  <head>
    <meta name="csrf-token" content="XZY">

Grab the token from the content property and use it in the request:

const token = document.head.querySelector('meta[name="csrf-token"]').content

const response = await fetch("/entities/1", {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ authenticity_token: token, entity: { name: "new name" } })
});

This is similar to @andrewle's answer but there's no need for an additional token.

thisismydesign
  • 14,900
  • 8
  • 89
  • 91
1

To add to Fernando's answer, if your controller responds to both json and html, you can use:

      skip_before_filter :verify_authenticity_token, if: :json_request?
user1756254
  • 369
  • 4
  • 6