38

I have a Rails 5 API app (ApplicationController < ActionController::API). The need came up to add a simple GUI form for one endpoint of this API.

Initially, I was getting ActionView::Template::Error undefined method protect_against_forgery? when I tried to render the form. I added include ActionController::RequestForgeryProtection and protect_from_forgery with:exception to that endpoint. Which solved that issue as expected.

However, when I try to submit this form I get: 422 Unprocessable Entity ActionController::InvalidAuthenticityToken. I've added <%= csrf_meta_tags %> and verified that meta: csrf-param and meta: csrf-token are present in my headers, and that authenticity_token is present in my form. (The tokens themselves are different from each other.)

I've tried, protect_from_forgery prepend: true, with:exception, no effect. I can "fix" this issue by commenting out: protect_from_forgery with:exception. But my understanding is that that is turning off CSRF protection on my form. (I want CSRF protection.)

What am I missing?

UPDATE:

To try to make this clear, 99% of this app is a pure JSON RESTful API. The need came up to add one HTML view and form to this app. So for one Controller I want to enable full CSRF protection. The rest of the app doesn't need CSRF and can remain unchanged.

UPDATE 2:

I just compared the page source of this app's HTML form and Header with another conventional Rails 5 app I wrote. The authenticity_token in the Header and the authenticity_token in the form are the same. In the API app I'm having the problem with, they're different. Maybe that's something?

UPDATE 3:

Ok, I don't the the mismatch is the issue. However, in further comparisons between the working and non-working apps I noticed that there's nothing in Network > Cookies. I see a bunch of things like _my_app-session in the cookies of the working app.

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
lostphilosopher
  • 4,361
  • 4
  • 28
  • 39

5 Answers5

38

Here's what the issue was: Rails 5, when in API mode, logically doesn't include the Cookie middleware. Without it, there's no Session key stored in a Cookie to be used when validating the token I passed with my form.

Somewhat confusingly, changing things in config/initializers/session_store.rb had no effect.

I eventually found the answer to that problem here: Adding cookie session store back to Rails API app, which led me here: https://github.com/rails/rails/pull/28009/files which mentioned exactly the lines I needed to add to application.rb to get working Cookies back:

config.session_store :cookie_store, key: "_YOUR_APP_session_#{Rails.env}"
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options

Those three lines coupled with:

class FooController < ApplicationController
  include ActionController::RequestForgeryProtection
  protect_from_forgery with: :exception, unless: -> { request.format.json? }
  ...

And of course a form generated through the proper helpers:

form_tag(FOO_CREATE_path, method: :post)
  ...

Got me a CSRF protected form in the middle of my Rails API app.

Community
  • 1
  • 1
lostphilosopher
  • 4,361
  • 4
  • 28
  • 39
  • 2
    It is not unexactly clear why you would need to use form_tag in API mode in the first place. The idea of API mode is have your backend as a pure data endpoint without being responsible for generating any UI representation. – Leo Lei Mar 28 '17 at 10:33
  • 1
    @FabrizioBertoglio No it's neither what I said, nor is your statement true. It is possible that you need to worry about CSRF attacks in API apps. One example that you need to worry about CSRF is that when your API service uses cookies. – Leo Lei Aug 09 '17 at 04:42
  • Now I get NoMethodError (undefined method `flash=' for #): using rails 5.1.7 api only app. Seems like adding config.middleware.use ActionDispatch::Flash helped. – Hugo Oct 24 '19 at 09:38
  • 1
    `request.format.json?` is not the check you want. A requester can bypass it by adding a `.json` to the end of a url they have a cross origin form posting to. You want `request.content_type ~= /json/` which will check the content-type header of the request, which the browser will prevent cross origin requests from having without CORS preflights. – Jesse Nov 26 '20 at 02:47
23

If you're using Rails 5 API mode, you do not use protect_from_forgery or include <%= csrf_meta_tags %> in any view since your API is 'stateless'. If you were going to use full Rails (not API mode) while ALSO using it as a REST API for other apps/clients, then you could do something like this:

protect_from_forgery unless: -> { request.format.json? }

So that protect_from_forgery would be called when appropriate. But I see ActionController::API in your code so it appears you're using API mode in which case you'd remove the method from your application controller altogether

Ricky Brown
  • 644
  • 5
  • 8
7

I had this challenge when working on a Rails 6 API only application.

Here's how I solved it:

First, include this in your app/controllers/application_controller.rb file:

class ApplicationController < ActionController::API
  include ActionController::RequestForgeryProtection
end

Note: This was added because protect_from_forgery is a class method included in ActionController::RequestForgeryProtection which is not available when working with Rails in API mode.

Next, add the cross-site request forgery protection:

class ApplicationController < ActionController::API
  include ActionController::RequestForgeryProtection

  protect_from_forgery with: :null_session
end

OR this if you want to protect_from_forgery conditionally based on the request format:

class ApplicationController < ActionController::API
  include ActionController::RequestForgeryProtection

  protect_from_forgery with: :exception if proc { |c| c.request.format != 'application/json' }
  protect_from_forgery with: :null_session if proc { |c| c.request.format == 'application/json' }
end

Finally, add the line below to your config/application.rb file. Add it inside the class Application < Rails::Application class, just at the bottom:

config.middleware.use ActionDispatch::Flash

So it will look like this:

module MyApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true

    config.middleware.use ActionDispatch::Flash
  end
end

Note: This will prevent the error below:

NoMethodError (undefined method `flash=' for #<ActionDispatch::Request:0x0000558a06b619e0>):

That's all.

I hope this helps

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
3

No need of protect_from_forgery for AJAX calls and apis.

If you want to disable it for some action then

protect_from_forgery except: ['action_name']
puneet18
  • 4,341
  • 2
  • 21
  • 27
  • Yes, my question is how to activate it for one Controller's worth of _non_-API calls? I have _one_ HTML page and form that I want CSRF protection on, the rest of the app is a RESTful JSON API and doesn't need it. I was assuming adding `protect_from_forgery` to that one controller, along with `include ActionController::RequestForgeryProtection` would do it, but now I'm getting `InvalidAuthenticityToken` when I try to submit that form. – lostphilosopher Mar 15 '17 at 16:18
  • Are you both really sure that you do not need CSRF protection in API only with Ajax calls and cookies? –  May 07 '18 at 20:17
  • 2
    How do you protect you APIs? –  May 07 '18 at 20:18
1
class Api::ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
end

Use as above with rails 5

Muktesh Kumar
  • 181
  • 2
  • 7