React_on_rails: Consider a CSRF replacement for $.ajax

Created on 5 Sep 2015  路  28Comments  路  Source: shakacode/react_on_rails

See:
https://github.com/shakacode/react-webpack-rails-tutorial/pull/73#issuecomment-137977029

We could provide a something helpful in this gem that facilitates true promises via 'fetch'.

up for grabs! easy enhancement

All 28 comments

My two cents. I use this:

import _ from 'underscore'

/* globals document */

export default class BaseData {
  constructor() {
    this.defaultParams = {
      credentials: 'same-origin'
    }
  }

  getCSRFToken() {
    return _.find(document.getElementsByTagName("meta"), (meta) => {
      return meta.name === "csrf-token"
    }).content
  }

  defaultHeaders = () => {
    return {
      'X-CSRF-Token': this.getCSRFToken(),
      'Accept': 'application/vnd.api+json',
      'Content-Type': 'application/vnd.api+json'
    }
  }

  post = (url, props) => {
    return fetch(url, {
      method: 'post',
      headers: this.defaultHeaders(),
      body: JSON.stringify(props),
      ...this.defaultParams
    })
    .then((response) => {
      return response.json()
    })
  }

  put = (url, props) => {
    return fetch(url, {
      method: 'put',
      headers: this.defaultHeaders(),
      body: JSON.stringify(props),
      ...this.defaultParams
    })
    .then((response) => {
      return response.json()
    })
  }

  delete = (url) => {
    return fetch(url, {
      method: 'delete',
      headers: this.defaultHeaders(),
      ...this.defaultParams
    })
    .then((response) => {
      return response.json()
    })
  }
}

And this module: "whatwg-fetch": "^0.9.0"

And in the webpack config:

new webpack.ProvidePlugin({
      'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
    })

We're using axios here:

https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/package.json

@alexfedoseev Are we missing the headers for:

     'Accept': 'application/vnd.api+json',
      'Content-Type': 'application/vnd.api+json'

Here's our source: https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client%2Fapp%2Futils%2FcommentsManager.js

import request from 'axios';

const API_URL = 'comments.json';

const CommentsManager = {

  /**
   * Retrieve comments from server using AJAX call.
   *
   * @returns {Promise} - result of ajax call.
   */
  fetchComments() {
    return request({
      method: 'GET',
      url: API_URL,
      responseType: 'json',
    });
  },

  /**
   * Submit new comment to server using AJAX call.
   *
   * @param {Object} comment - Comment body to post.
   * @returns {Promise} - result of ajax call.
   */
  submitComment(comment) {
    return request({
      method: 'POST',
      url: API_URL,
      responseType: 'json',
      headers: {
        'X-CSRF-Token': this.getCSRFToken(),
      },
      data: { comment },
    });
  },

  /**
   * Get CSRF Token from the DOM.
   *
   * @returns {String} - CSRF Token.
   */
  getCSRFToken() {
    const metas = document.getElementsByTagName('meta');
    for (let i = 0; i < metas.length; i++) {
      const meta = metas[i];
      if (meta.getAttribute('name') === 'csrf-token') {
        return meta.getAttribute('content');
      }
    }

    return null;
  },

};

export default CommentsManager;

Are we missing the headers for

No, since we don't use Vendor MIME Type to match the api version (we use URL instead).
More on subject: https://medium.com/@alexfedoseev/isomorphic-react-with-rails-part-ii-614980b65aef

And example of apiCall helper with axios:

import http from 'axios';
import getCSRFToken from './getCSRFToken';

const apiURLPrefix = '/api/v1';

// Transforming Immutable data to plain JS
http.defaults.transformRequest.unshift(data => (
  data && data.toJS === 'function' ? data.toJS() : data
));

http.interceptors.request.use(config => {
  // If it's not remote call, adding api prefix
  if (!config.remote) {
    config.url = `${apiURLPrefix}${config.url}`;
  }

  // Adding CSRF Token header
  config.headers = {
    'X-CSRF-Token': getCSRFToken(),
  };
  return config;
});

export default params => http(params);

:+1: I'm using that because I'm using a jsonapi-resources backend, which requires that MIME type

@alexfedoseev @josiasds @mapreal19 How about we release a tiny npm module to encapsulation axios and the ability to get the CRSF token from Rails? Then we can put the inclusion of the tiny npm module in the template setup.

I'd rather made getCRSFToken module and left axios in user land.

Reasons:

  1. axios is opinionated.
  2. axios's setup is opinionated.

This is probably the one we want to to support:

https://github.com/matthew-andrews/isomorphic-fetch

@jbhatab @alexfedoseev Once we put in the NPM library, I'll try to merge in the code from the: https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/libs/metaTagsManager.js

@justin808 @alexfedoseev @jbhatab

Regardless though, I vote we do not put this in the npm module for the same reason that Alex chose not to use Axios's implementation. Some people may not even be doing sessions this way.

Just to leave another implementation here for the sake of brainstorm. We only use helper methods that don't wrap fetch, but aid in parameter setting. Implementation in this gist.

For regular non-safe json requests:

fetch('/foobar.json', jsonHeader(/*fetch options*/));

With just the Rails CSRF headers:

fetch('/foobar.json', authenticityHeader(/*fetch options*/));

For safe calls (includes CSRF and credentials & same-origin header):

fetch('/foobar.json', safeCredentials(/*fetch options*/));

We'll soon be adding a JS helper method for the CRSF to the ReactOnRails npm module for this.

We probably want to be agnostic about the choice of fetch vs. axios.

agreed

We simply need to add this helper method to ReactOnRails.

From: https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/libs/metaTagsManager.js

import _ from 'lodash';

export default {

  /**
   * Get CSRF Token from the DOM.
   *
   * @returns {String} - CSRF Token.
   */
  getCSRFToken() {
    const token = _.find(document.querySelectorAll('meta'), ['name', 'csrf-token']);
    return token ? token.content : null;
  },

};

To:

We should also remove the dependency on lodash.

We need some docs and tests.

This gist from @fletcher91 also looks good: https://gist.github.com/fletcher91/c62b865c6aa9f710531e

I'll probably add @fletcher91's code to the default package for ReactOnRails. Yes, we could have a separate node module. It's a tiny enough amount of code that I don't deem that necessary.

@fletcher91 Any changes to your gist? I copied below.

/**
 * 
 * A regular non-safe get request:
 * fetch('/profiles/foobar.json', jsonHeader());
 * 
 * How this would look in a safe fetch request:
 * fetch('/profiles.json', safeCredentials({
 *              method: 'POST',
 *              body: JSON.stringify({
 *                  q: input,
 *                  thing: this.props.thing
 *              })
 *          }));
 *
 * 
 */

/**
 * For use with window.fetch
 */
export function jsonHeader (options) {
    options = options || {};
    return Object.assign(options, {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    });
}

/**
 * Lets fetch include credentials in the request. This includes cookies and other possibly sensitive data.
 * Note: Never use for requests across (untrusted) domains.
 */
export function safeCredentials (options) {
    options = options || {};
    return Object.assign(options, {
        credentials: 'include',
        mode: 'same-origin',
        headers: Object.assign((options['headers'] || {}), authenticityHeader(), jsonHeader())
    });
}

// Additional helper methods

export function authenticityHeader (options) {
    options = options || {};
    return Object.assign(options, {
        'X-CSRF-Token': getAuthenticityToken(),
        'X-Requested-With': 'XMLHttpRequest'
    });
}

export function getAuthenticityToken () {
    return getMetaContent('csrf-token');
}

export function getMetaContent (name) {
    let header = document.querySelector(`meta[name="${name}"]`);
    return header && header.content;
}

@justin808 Aside from some minor things like let to const and some JSdocs, it's still the same 馃憤 I've updated the gist accordingly

@fletcher91, @jbhatab does this make sense to go into React on Rails from the perspective that the CRSF thing is so standard for Rails apps? or more of a doc thing?

I'd be happy to have this as part of react_on_rails. It's really a very common requirement.

@anpr With the direction were going, I think this shouldn't be covered with our gem and can be covered in a docs if even needed at all. it depends on your architecture style.

We can relook into this when we redo the docs stuff.

@jbhatab, we're going to provide a default for this. I really think this is so common that it makes sense to include in our library, if it's non-obtrusive.

@anpr I can help you with this. Please email me at [email protected] and I can add you to our Slack room.

We're working on restructuring our front-end (splitting into packages), so I can publish these helpers publicly to NPM if that helps

I basically copied the code above to my project and it solved the problem (with small adjustments/additions).

Still, I see this being a common requirement, so I'd publish it either as part of the gem or as a separate npm package (I don't really care which of the two).

@anpr I think part of the standard npm package for react-on-rails is fine if it's very small.

@dzirtusss Please take a look at this one.

@fletcher91 @anpr @alexfedoseev @dphaener @samnang @dzirtusss: we'd like to know if the headers here: https://gist.github.com/fletcher91/c62b865c6aa9f710531e
should be here: https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/libs/requestsManager.js#L33

2016-08-10_12-13-20

Actually I think we should not need to include jsonHelper into gem and specifically include json Accept and inContent-Type into headers because it is already included by ??? as follows:

Content-Type: application/json;charset=UTF-8
Accept: application/json, text/plain, */*
Cookie: _rails-react-tutorial_session=...

And everything works fine.

Maybe it will not work with fetch same way but will need to update first from axios to isomorphic-fetch in tutorial.

I think it will be ok if we use fetch like:

credentials: 'same-origin',
header: RubyOnRails.authenticityHeader(),

Resolved by #517

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CodyFitzpatrick picture CodyFitzpatrick  路  6Comments

jtibbertsma picture jtibbertsma  路  7Comments

robwise picture robwise  路  3Comments

serodriguez68 picture serodriguez68  路  7Comments

hakunin picture hakunin  路  3Comments