Rails: How to implement protect_from_forgery in Rails API mode

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


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.


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