From 52104d5de546142459a871af24ae1d0f0ad2df61 Mon Sep 17 00:00:00 2001 From: Sergey Tarasov Date: Thu, 11 Aug 2016 11:43:21 +0300 Subject: [PATCH] added CSRF helpers authenticityToken and authenticityHeaders --- CHANGELOG.md | 1 + README.md | 16 +++++++++++++ docs/api/javascript-api.md | 23 +++++++++++++++++++ node_package/src/Authenticity.js | 14 ++++++++++++ node_package/src/ReactOnRails.js | 26 +++++++++++++++++++++ node_package/tests/Authenticity.test.js | 30 +++++++++++++++++++++++++ node_package/tests/ReactOnRails.test.js | 1 + 7 files changed, 111 insertions(+) create mode 100644 node_package/src/Authenticity.js create mode 100644 node_package/tests/Authenticity.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index cee1a062d..77b4b6142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com] - React on Rails server rendering now supports contexts outside of browser rendering, such as ActionMailer templates [#486](https://github.com/shakacode/react_on_rails/pull/486) by [eacaps](https://github.com/eacaps). - React on Rails now correctly parses single-digit version strings from package.json [#491](https://github.com/shakacode/react_on_rails/pull/491) by [samphilipd ](https://github.com/samphilipd ). - Fixed assets symlinking to correctly use filenames with spaces. Begining in [#510](https://github.com/shakacode/react_on_rails/pull/510), ending in [#513](https://github.com/shakacode/react_on_rails/pull/513) by [dzirtusss](https://github.com/dzirtusss) +- Added authenticityToken() and authenticityHeaders() javascript helpers for easier use when working with CSRF protection tag generated by Rails [#517](https://github.com/shakacode/react_on_rails/pull/517) by [dzirtusss](https://github.com/dzirtusss) ## [6.0.5] - 2016-07-11 ##### Added diff --git a/README.md b/README.md index f8f35edf4..8320bb5b7 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,22 @@ Components are created as [stateless function(al) components](https://facebook.g ## ReactOnRails JavaScript API See [ReactOnRails JavaScriptAPI](docs/api/javascript-api.md). +#### Using Rails built-in CSRF protection in JavaScript + +Rails has built-in protection for Cross-Site Request Forgery (CSRF), see [Rails Documentation](http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf). To nicely utilize this feature in JavaScript requests, React on Rails is offerring two helpers that can be used as following for POST, PULL or DELETE requests: + +``` +import ReactOnRails from 'react-on-rails'; + +// reads from DOM csrf token generated by Rails in <%= csrf_meta_tags %> +csrfToken = ReactOnRails.authenticityToken(); + +// compose Rails specific request header as following { X-CSRF-Token: csrfToken, X-Requested-With: XMLHttpRequest } +header = ReactOnRails.authenticityHeaders(otherHeader); +``` + +If you are using [jquery-ujs](https://github.com/rails/jquery-ujs) for AJAX calls, than these helpers are not needed because the [jquery-ujs](https://github.com/rails/jquery-ujs) library updates header automatically, see [jquery-ujs documentation](https://robots.thoughtbot.com/a-tour-of-rails-jquery-ujs#cross-site-request-forgery-protection). + ## React Router [React Router](https://github.com/reactjs/react-router) is supported, including server side rendering! See: diff --git a/docs/api/javascript-api.md b/docs/api/javascript-api.md index a2d078ecd..49f608429 100644 --- a/docs/api/javascript-api.md +++ b/docs/api/javascript-api.md @@ -34,4 +34,27 @@ The best source of docs is the main [ReactOnRails.js](../../node_package/src/Rea * `traceTurbolinks: true|false Gives you debugging messages on Turbolinks events */ setOptions(options) + + /** + * Allow directly calling the page loaded script in case the default events that trigger react + * rendering are not sufficient, such as when loading JavaScript asynchronously with TurboLinks: + * More details can be found here: + * https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/turbolinks.md + */ + reactOnRailsPageLoaded() + + /** + * Returns CSRF authenticity token inserted by Rails csrf_meta_tags + * @returns String or null + */ + + authenticityToken() + + /** + * Returns header with csrf authenticity token and XMLHttpRequest + * @param {*} other headers + * @returns {*} header + */ + + authenticityHeaders(otherHeaders = {}) ``` diff --git a/node_package/src/Authenticity.js b/node_package/src/Authenticity.js new file mode 100644 index 000000000..24f9eac67 --- /dev/null +++ b/node_package/src/Authenticity.js @@ -0,0 +1,14 @@ +export default { + + authenticityToken() { + const token = document.querySelector('meta[name="csrf-token"]'); + return token ? token.content : null; + }, + + authenticityHeaders(otherHeaders = {}) { + return Object.assign(otherHeaders, { + 'X-CSRF-Token': this.authenticityToken(), + 'X-Requested-With': 'XMLHttpRequest' + }); + }, +}; diff --git a/node_package/src/ReactOnRails.js b/node_package/src/ReactOnRails.js index 653fb4b15..f167c894e 100644 --- a/node_package/src/ReactOnRails.js +++ b/node_package/src/ReactOnRails.js @@ -5,6 +5,7 @@ import StoreRegistry from './StoreRegistry'; import serverRenderReactComponent from './serverRenderReactComponent'; import buildConsoleReplay from './buildConsoleReplay'; import createReactElement from './createReactElement'; +import Authenticity from './Authenticity'; import ReactDOM from 'react-dom'; import context from './context'; @@ -68,10 +69,35 @@ ctx.ReactOnRails = { } }, + /** + * Allow directly calling the page loaded script in case the default events that trigger react + * rendering are not sufficient, such as when loading JavaScript asynchronously with TurboLinks: + * More details can be found here: + * https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/turbolinks.md + */ reactOnRailsPageLoaded() { ClientStartup.reactOnRailsPageLoaded(); }, + /** + * Returns CSRF authenticity token inserted by Rails csrf_meta_tags + * @returns String or null + */ + + authenticityToken() { + return Authenticity.authenticityToken(); + }, + + /** + * Returns header with csrf authenticity token and XMLHttpRequest + * @param {*} other headers + * @returns {*} header + */ + + authenticityHeaders(otherHeaders = {}) { + return Authenticity.authenticityHeaders(otherHeaders); + }, + //////////////////////////////////////////////////////////////////////////////// // INTERNALLY USED APIs //////////////////////////////////////////////////////////////////////////////// diff --git a/node_package/tests/Authenticity.test.js b/node_package/tests/Authenticity.test.js new file mode 100644 index 000000000..81ade244a --- /dev/null +++ b/node_package/tests/Authenticity.test.js @@ -0,0 +1,30 @@ +import test from 'tape'; +import ReactOnRails from '../src/ReactOnRails.js'; + +test('authenticityToken and authenticityHeaders', (assert) => { + assert.plan(4); + + assert.ok(typeof ReactOnRails.authenticityToken == 'function', + 'authenticityToken function exists in ReactOnRails API'); + + assert.ok(typeof ReactOnRails.authenticityHeaders == 'function', + 'authenticityHeaders function exists in ReactOnRails API'); + + const testToken = 'TEST_CSRF_TOKEN'; + + var meta = document.createElement('meta'); + meta.name = 'csrf-token'; + meta.content = testToken; + document.head.appendChild(meta); + + var realToken = ReactOnRails.authenticityToken(); + + assert.equal(realToken, testToken, + 'authenticityToken can read Rails CSRF token from '); + + var realHeader = ReactOnRails.authenticityHeaders(); + + assert.deepEqual(realHeader, { 'X-CSRF-Token': testToken, 'X-Requested-With': 'XMLHttpRequest' }, + 'authenticityHeaders returns valid header with CFRS token' + ); +}); diff --git a/node_package/tests/ReactOnRails.test.js b/node_package/tests/ReactOnRails.test.js index 07a3bc944..ab01080f1 100644 --- a/node_package/tests/ReactOnRails.test.js +++ b/node_package/tests/ReactOnRails.test.js @@ -124,3 +124,4 @@ test('setStore and getStore', (assert) => { assert.deepEqual(ReactOnRails.stores(), expected); }); +