Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing X-CSRF-Token (Ruby on Rails) #424

Closed
peric opened this issue Nov 1, 2016 · 14 comments
Closed

Passing X-CSRF-Token (Ruby on Rails) #424

peric opened this issue Nov 1, 2016 · 14 comments

Comments

@peric
Copy link

peric commented Nov 1, 2016

In Ruby on Rails, you need to pass X-CSRF-Token in POST request header in order to properly handle the form. jquery-ujs basically handles that by default, but if you are not using it or you want to send requests with some other libraries (like fetch), you need to pass this header by your own.

In jQuery, this can be handle (and this works) like:

var token = $('meta[name="csrf-token"]').attr('content');

$.ajax({
    url: '/somecustomurl',
    type: 'post',
    beforeSend: function (xhr) {
        xhr.setRequestHeader('X-CSRF-Token', token)
    },
    data: {},
    contentType: false,
    processData: false
});

With fetch, I've tried:

fetch('/somecustomurl', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': token
  },
  body: JSON.stringify({})
})

And this example never works - controller will always raise a InvalidAuthenticityToken exception.

Any ideas how can this be solved with fetch?

@mislav
Copy link
Contributor

mislav commented Nov 1, 2016

Passing x-csrf-token header should work as setting any other custom header. Your method of specifying headers for the fetch() request seems correct from what you've shared with us. The problem must be elsewhere.

Are you sure the correct token gets passed? Can you use Chrome devtools or a debugging HTTP proxy to inspect the actual HTTP headers sent with the fetch POST request?

@peric
Copy link
Author

peric commented Nov 1, 2016

Hey Mislav, thanks for the answer.

Yes, I'm quite sure everything is properly passed - token is the same as the one in jQuery post request.

I've added skip_before_action :verify_authenticity_token to my controller and then checked parameters that are sent, together with a x-csrf-token in request header. Everything looks proper in both examples (jQuery and fetch), but fetch just doesn't work.

I've done some additional debugging and comparing between those two requests, then I've tried to send the same headers but didn't work. There's a "Cookie" sent in jQuery request header (which is not sent in fetch) and some other headers are bit different...don't know if this could be the reason.

Anyway, I'll go with jQuery here. Thanks for helping out.

@peric peric closed this as completed Nov 1, 2016
@mislav
Copy link
Contributor

mislav commented Nov 1, 2016

@origamih
Copy link

origamih commented Nov 8, 2016

@peric You need to set header props: 'X-Requested-With': 'XMLHttpRequest', and credentials: 'same-origin'.

fetch(url, { 
      method: 'POST',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'X-CSRF-Token': token
      },
      body: ,
      credentials: 'same-origin'
    })

Updated: to make rails accept JSON post, add the following code to header:

'Content-Type': 'application/json',
        'Accept': 'application/json'

@peric
Copy link
Author

peric commented Nov 8, 2016

I went back to jQuery for this, but thanks!

@origamih
Copy link

Yeah, jQuery is a very good solution. The only problem is when you test, you need a DOM to make jQuery work (jsdom...) which I don't want to import to all my redux tests. But anyways those headers look ugly

@mislav
Copy link
Contributor

mislav commented Nov 11, 2016

You could wrap fetch in your app to set those headers by default:

var originalFetch = window.fetch

function myFetch(req, init) {
  var request = new Request(req, init)
  request.headers.set('X-Requested-With', 'XMLHttpRequest')
  if (request.method !== 'GET') init.headers.set('X-CSRF-Token', getCsrfToken())
  request.credentials = 'same-origin'
  return originalFetch(request)
}

You could even override window.fetch = myFetch with your wrapper. Careful, because my code above is untested, and will not handle some edge-cases, like making requests to other domains.

@adelivuk-zz
Copy link

adelivuk-zz commented Apr 8, 2017

As RoR5 has Per-form CSRF Tokens:

Rails 5 now supports per-form CSRF tokens to mitigate against code-injection attacks with forms created by JavaScript. With this option turned on, forms in your application will each have their own CSRF token that is specified to the action and method for that form.
config.action_controller.per_form_csrf_tokens = true

How will we be able to use fetch now, to send each forms own token?

One solution is to set this option to false and use the main token, however, that doesn't seem to be the RoR5 way?

@mislav
Copy link
Contributor

mislav commented Apr 11, 2017

How will we be able to use fetch now, to send each forms own token?

The way we do this in GitHub.com is to generate an actual <form> element on the server, via Rails' form_tag helper. Each form generated that way also includes the per-form token. Then, if we need to post that form using JavaScript, we simply use FormData:

fetch(form.action, {
  method: 'POST',
  body: new FormData(form)
})

@nikolas
Copy link

nikolas commented Sep 21, 2017

Thanks for these steps. After following them I got mildly frustrated, but then realized my problem is that Django expects the header to be called X-CSRFToken, not X-CSRF-Token: https://docs.djangoproject.com/en/1.11/ref/csrf/#setting-the-token-on-the-ajax-request

@simioluwatomi
Copy link

I just want to say that this works on Laravel too but you gotta add

credentials: "same-origin",

to the request.

@lmiller1990
Copy link

Thanks @simioluwatomi , this was the problem for me too.

full code (coffescript)

buildRequest: ()->
          headers = new Headers()
          headers.append('X-Requested-With', 'XMLHttpRequest')
          headers.append('X-CSRF-TOKEN', @csrfToken)
          headers.append('Content-Type', 'application/json')
          new Request('/blogs', {
            headers: headers,
            credentials: 'same-origin',
            method: 'POST',
            body: JSON.stringify(
              "blog": {
                title: @blog.title,
                body: @blog.body
              }
            )
          })

@aaronlifton
Copy link

aaronlifton commented Apr 11, 2018

here's an updated typescript version of @origamih 's snippet

function apiFetch(req: string | Request, init: RequestInit) {
  init = Object.assign(init, {
    credentials: 'same-origin',
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/json'
    },
  });
  const csrfToken = 'TEST123';
  let request = new Request(req, init);

  if (request.method != 'GET')
    init.headers['X-CSRF-Token'] = csrfToken;

  return fetch(request, init);
}

@svnm
Copy link

svnm commented Oct 17, 2018

I also ran into this issue in rails, and had to modify @origamih's code, then it worked nicely. I set up a function which I export and use to wrap fetch requests, as shown below:

const getHeaders = () => {
  let headers = new window.Headers({
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  })

  const csrfToken = document.head.querySelector("[name='csrf-token']")
  if (csrfToken) { headers.append('X-CSRF-Token', csrfToken) }

  return headers
}

export const createRequest = (url, method = 'get') {
  const request = new window.Request(url, {
    headers: getHeaders(),
    method: method,
    credentials: 'same-origin',
    dataType: 'json'
  })
  return request
}

And use it like so:

import { createRequest } from './lib/createRequest'

fetch(createRequest('my-url', 'get'))
  .then(response => response.json())
  .then(data => {
    console.log(data)
  })

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants