15

I need to use a Single Page Application (React, Ember, Angular, I don't care) with Rails CSRF protection mechanism.

I'm wondering if I need to create a token evey time in the ApplicationController like this:

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
    cookies["X-CSRF-Token"] = form_authenticity_token
  end

end

or I can just create a token once.

Per session or per (non-GET) request?

I think the token is still valid until the session is valid, right?

CLARIFY:

I see Rails default application (server-rendered pages) update csrf-token each time I navigate a page. So every time it changes.

So in my situation if I create a new token for each after_action the previous CSRF-Token is still good for that session. So, how to invalidate the previous token? I have to?

Because only if I invalidate it makes sense, right?

  • 1
    Interesting read: https://github.com/equivalent/scrapbook2/blob/master/archive/blogs/2017-10-12-csrf-protection-on-single-page-app-api.md – nathanvda May 06 '18 at 22:59
  • 1
    Thanks @nathanvda, but I already read that page and all the links at the bottom and all the Google's servers know me as the best man in the world on this argument. But on this I still don't have the answer! –  May 06 '18 at 23:01
  • Just so I fully understand the context, are you using Devise or any other authentication gem with this ? – equivalent8 May 11 '18 at 20:58
  • Devise gem only. –  May 11 '18 at 21:00

4 Answers4

24

Client side (SPA)

You only need to grab the CSRF token once per session. You can hold onto it in the browser and send it on every (non-GET) request.

Rails will appear to generate a new CSRF token on every request, but it will accept any generated token from that session. In reality, it is just masking a single token using a one-time pad per request, in order to protect against SSL BREACH attack. More details at https://stackoverflow.com/a/49783739/2016618. You don't need to track/store these tokens.

Server side

I strongly suggest using Rails's protect_from_forgery directive rather than encoding the CSRF token in a header yourself. It will generate a different masked token per request.

You can certainly reproduce this yourself with not that much code, but I don't see why you'd need to.

Do you need CSRF protection with an API?

Yes! If you are authenticating with a cookie, you need CSRF protection. This is because cookies are sent with every request, so a malicious website could send a POST request to your site and perform requests on behalf of a logged in user. The CSRF token prevents this, because the malicious site won't know the CSRF token.

Sarkom
  • 823
  • 10
  • 12
  • This attack from a POST request would only be accessible if your endpoint is accepting `application/x-www-form-urlencoded` correct? I would imagine for many RESTful API's, they are only allowing for POSTs with `application/json`. – seansean11 Sep 11 '19 at 22:15
  • 1
    @seansean11 Sure, if you only allow that content type then you do not need CSRF, but the OP asked specifically about a default Rails application. – Sarkom Sep 11 '19 at 22:55
  • 1
    The malicious website could also send a request to the webserver to get the CSRF token and then POST whatever. – Basheer Kharoti Oct 27 '20 at 08:23
  • @BasheerKharoti No, they should not be able to. "Cross-origin reads are typically disallowed" (https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy). – Sarkom Oct 28 '20 at 13:53
  • @Sarkom CORS (Cross-Origin Resource Sharing) and SOP (Same-Origin Policy) are server-side configurations that clients decide to enforce or not. [Read this](https://stackoverflow.com/questions/36250615/cors-with-postman) – Basheer Kharoti Oct 31 '20 at 07:46
  • 1
    @BasheerKharoti Same-Origin Policy is a standard feature of all major browsers and has been for many years now. It is true that these protections can be bypassed by using a non-browser client or a developer tool like Postman, but **malicious websites are not able to use these tools to get around SOP restrictions**. If you know of any exceptions to this, please share them. – Sarkom Nov 01 '20 at 17:55
  • 1
    @Sarkom Not necessarily to be a non-browser client but can also be bypassed in any browser. [Bypass chrome](https://alfilatov.com/posts/run-chrome-without-cors/) – Basheer Kharoti Nov 02 '20 at 04:37
2

I'll just answer the question. Full details are explained in this article: https://blog.eq8.eu/article/rails-api-authentication-with-spa-csrf-tokens.html

Given Rails is setting cookie for SPA

CSRF tokens are valid during the lifetime of the session. Therefore yes it's ok just to generate one CSRF token for the duration of session after login.

class LoginController < ApplicationController
  def create
    if user_password_match
      # ....
      cookie[:my_csrf_token]= form_authenticity_token
    end
  end
end

or you can refresh the cookie the same way as you are proposing

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
    cookies["my_csrf_token"] = form_authenticity_token
  end

end

Your SPA just needs to read the cookie and set it as header.

Both are equally valid

or You can just provide the CSRF token as a login response and SPA will store it somewhere and use it in the X-CSRF-Token header:

curl POST  https://api.my-app.com/login.json  -d "{"email":'equivalent@eq8.eu", "password":"Hello"}"  -H 'ContentType: application/json'
# Cookie with session_id was set

# response:

{  "login": "ok", "csrf": 'yyyyyyyyy" }

# next request 

 curl POST  https://api.my-app.com/transfer_my_money  -d "{"to_user_id:":"1234"}"  -H "ContentType: application/json" -H "X-CSRF-Token: yyyyyyyyy"

Given Rails is accepting header only authentication

if you are not using cookie to send session_id, therefore your API is using just Authentication header for authentication. Then you don't need CSRF protection.

No session cookie no CSRF problems !

Example:

curl POST  https://api.my-app.com/login.json  -d "{"email":'equivalent@eq8.eu", "password":"Hello"}"  -H 'ContentType: application/json'
# No cookie/session is set

# response:
{ "login": "ok", "jwt": "xxxxxxxxxxx.xxxxxxxx.xxxxx" }

# Next Request:
curl POST  https://api.my-app.com/transfer_my_money  -d "{"to_user_id:":"1234"}"  -H "ContentType: application/json" -H "Authentication: Bearer xxxxxxxxxxx.xxxxxxxx.xxxxx"

Once again, this is only when you don't use cookies for user identification ! So CSRF is not an issue in this case but you still protect from Cross site scripting attack, make sure your communication is HTTPs only, etc...

equivalent8
  • 13,754
  • 8
  • 81
  • 109
1

I don’t know what exact issue you are facing. But if you are getting CSRF issues in New Rails versions and need to include Rails CSRF tokens in ajax requests you can follow the steps below.

Recently I used Rails 5.1 application.

When using ajax calls to fetch some data from APIs I was getting CSRF token issues:

‘WARNING: Can't verify CSRF token authenticity rails’

The reason was

Rails 5.1 removed support for jquery and jquery_ujs by default, and added

//= require rails-ujs in application.js

It does the following things:

  1. force confirmation dialogs for various actions;
  2. make non-GET requests from hyperlinks;
  3. make forms or hyperlinks submit data asynchronously with Ajax;
  4. have submit buttons become automatically disabled on form submit to prevent double-clicking. (from: https://github.com/rails/rails-ujs/tree/master)

But it is not including the csrf token for ajax request by default. Beware of that. We have to explicitly pass it like:

$( document ).ready(function() {
  $.ajaxSetup({
    headers: {
      'X-CSRF-Token': Rails.csrfToken()
    }
  });
  ----
  ----
});

Note that in Rails 5.1 version, you get ‘Rails’ class in js, and can make use of the functions.

Update: If you are using Rails server side and other front end, you really don't want to use Rails provided CSRF tokens. Because Its really not matter which backend service you are using.

If your aim is to block CSRF, you need to set up CORS in backend, that is your Rails backend. Rails is now providing a separate file in initializer for this. You can mention which sites are allowed to send ajax requests to your backend.

Edit here:

config/initializers/cors.rb

If you want authentication, use basic auth, token auth Or JWT

Abhi
  • 3,361
  • 2
  • 33
  • 38
  • Dear @Abhi, I'm not using Rails for frontend pages. I'm using Ember, Angular, React, Vue. –  May 07 '18 at 10:36
-3

If you go with SPA application then you mostly use your Rails only as an API. CSRF token was designed for server rendering... not SPA. In SPA you already use token during authentication, so no need to use another token for CSRF. CSRF was designed as a protection for cross site calls, but API itself designed in a way that it allows request from anywhere until, they are authenticated.

Just disable it for your API and that's all. I would go with some API namespace and setup a BaseController, that will be inherited for all API controllers. There you should set protect_from_forgery:

class API::BaseController < ApplicationController
  protect_from_forgery with: :null_session
end
AntonTkachov
  • 1,784
  • 1
  • 10
  • 29
  • I don't understand. CSRF is only for server-rendered pages? But what if I have my API besides cookies authentication? Using devise for example? And my client is just my SPA? I don't wanna use JWT or similar. So I think with cookie based sessions I need CSRF also with my SPA and my Rails API, right? –  May 03 '18 at 18:01
  • "CSRF is only for server-rendered pages?" -> Yes; "But what if I have my API besides cookies authentication?" -> Then it's just an ajax request, not an API way (my point of view); "Using devise for example?" -> Devise has extension gems for token authentication; "And my client is just my SPA?" -> Anything. Mobile devices/other servers... anything, that is authenticated with token or visit your public endpoints; "I don't wanna use JWT or similar." -> Why?; "So I think with cookie based sessions I need CSRF also with my SPA and my Rails API, right?" -> This will force you to load CSRF every time – AntonTkachov May 03 '18 at 18:06
  • What do you think about this: https://stackoverflow.com/a/15056471/4412054? –  May 03 '18 at 19:06
  • I do not prefer to use JWT, beacuse of all these reasons: http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/ –  May 03 '18 at 19:07
  • What you call `ajax calls` is what SPA framework (Ember, React, Angular) call `API requests`? –  May 03 '18 at 19:07
  • Here it is another question about this topic: https://stackoverflow.com/questions/50134071/authenticate-apis-for-all-clients-type-with-one-or-many-methods –  May 03 '18 at 19:09
  • To be honest you provided me huge reading :) What I think during rough research -- If smdb still your JWT token, then in 95% cases that will have your CSRF token too.... so I still don't think it matters. Also, rails core team is smarter then both you and me.... I think that sticking to default is a valid way unless you need some really none-default app – AntonTkachov May 03 '18 at 20:21
  • Somebody has to still csrf-token AND has to to cross site attack. With JWT just some malicious javascript code and all that problems. So the problem is: also the API with CSRF if using it via browser. –  May 04 '18 at 07:50