The angularjs client reads the CSRF token that must be sent with AJAX requests from a cookie, instead of reading it from a meta tag in the DOM.

To protect from CSRF attacks, the client must send a valid CSRF token with POST, PUT or DELETE http requests. This token is a value set by the server which is associated with the Rails session, and does not change during the session lifetime.

Previous to this commit, the token was embedded in the DOM; specifically the <%= csrf_meta_tags %> in layouts/_header.html.erb outputs a <meta name="csrf-token"> tag in the html <head>; the content of this meta tag is the CSRF token. The angularjs client read this token from the DOM on application initialization (see ng-modules/ng-app.js.coffee) and then sent it on each AJAX request in the X-CSRF-Token http header. Rails expects this header for POST/PUT/DELETE requests and the protect_from_forgery filter in application_controller.rb validates that the token sent matches the one stored in the session.

However this poses a problem in our app, which uses the devise-rememberable module. This module sets a persistent "remember_user_token" cookie; if a user tries to access the app with a timed-out rails session but the "remember_user_token" cookie is valid, a new Rails session is transparently created for him. The result is that when the user reopens the browser after a long time, he's still logged in into the app, which is generally desirable (the user has the option to disable this behavior by unchecking the "remember me" checkbox in the login form).

In our app, this means that if the user has a new session transparently created by devise-rememberable, he's still seeing an HTML page with the old CSRF token embedded, because no page reload will happen. The old CSRF token is no longer valid, because the Rails session he's using now is a different one than the one from when the page was sent to him. The user will be able to see feeds and even see entries in any feed, because those are retrieved via GET AJAX requests (the CSRF token is not validated for GET requests, because they should be idempotent and never change data in the server). However as soon as he opens any entry, an AJAX POST will be sent  (to change the entry state to "read"); the CSRF validation will fail, an HTTP 401 will be sent and the user will be redirected to the login page.

The conclusion is that the CSRF token embedded in the DOM cannot be trusted, because thanks to Devise-rememberable it may have been generated during an already expired Rails session, and no longer be valid.

The fix is twofold:

- The server sets a cookie with each request, storing the CSRF token in the cookie. This is safe against CSRF attacks because only javascript sent by our app can read the cookie (it would be vulnerable to XSS attacks, though). The XSRF-TOKEN name is used for the cookie because it's the one expected by AngularJS, and no extra configuration is needed in the client to make it send the value in an HTTP header during each subsequent request. If the client ever sends an invalid XSRF-TOKEN cookie, the server will send back the correct cookie value, and as long as the client sends the correct cookie it won't be sent again by the server; this is normal cookies behavior and is not considered an error.

- The angularjs client reads the XSRF-TOKEN cookie and sends it in an HTTP header with each AJAX request. AngularJS does this by default if the cookie is present, without need for configuration. The only change in config necessary is to change the name that AngularJS uses by default for the header, "X-XSRF-TOKEN", to the header name expected by the protect_from_forgery Rails filter, "X-CSRF-Token".

With this change when the user opens the browser after a long time, if the first AJAX request performed is a GET, Devise-rememberable will create a new Rails session and the new CSRF token will be sent as a cookie in the response, so that the correct token is sent in subsequent requests. The token used in this first GET will be old and no longer valid, but the CSRF token in the HTTP header is not validated for GET requests, and the invalid value in the cookie will mean that the server will just respond with the correct cookie value, instead of being treated as an error. This way the user will be able to transparently resume using the app.

I estimate that in most cases the first action from a user will be an AJAX GET (either get the list of entries for a feed/folder, or an automated refresh of feeds/folders). In the rare cases in which the first action from a user is to generate a POST/PUT/DELETE (e.g. open an entry) the behavior will be as above (the user will be redirected to the login form). This should be rare but I may have to address it in the future.

I'm keeping the CSRF token in the <meta> tags because I currently see no value to remove it and it could cause problems with some non-AJAX forms.

To read more about angularJS handling of CSRF tokens in cookies:

https://docs.angularjs.org/api/ng/service/$http#description_security-considerations_cross-site-request-forgery-protection

To read interesting discussions about this topic:

http://technpol.wordpress.com/2014/04/17/rails4-angularjs-csrf-and-devise/
http://stackoverflow.com/questions/14734243/rails-csrf-protection-angular-js-protect-from-forgery-makes-me-to-log-out-on
http://stackoverflow.com/questions/11845500/rails-devise-authentication-csrf-issue

(note that unlike some of the above stackoverflow users, in this app a logout means a page load, which generates a new CSRF token both in the DOM and the cookie. Therefore I have no need to override Devise controller methods).