Rails: How to implement protect_from_forgery in Rails API mode
Asked Answered
N

5

40

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.

Nubilous answered 14/3, 2017 at 19:50 Comment(0)
N
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.

Nubilous answered 16/3, 2017 at 21:4 Comment(4)
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.Nowicki
@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.Nowicki
Now I get NoMethodError (undefined method `flash=' for #<ActionDispatch::Request:0x00007f8d66bbe0b8>): using rails 5.1.7 api only app. Seems like adding config.middleware.use ActionDispatch::Flash helped.Abiotic
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.Novel
G
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

Goar answered 15/3, 2017 at 8:5 Comment(6)
The app was a pure RESTful API, all json. Now there's a need to add one HTML view and form to this app. So, just for that Controller I want to switch out of API mode and enable CSRF protection.Nubilous
I did try this solution, but I still get Can't verify CSRF token authenticity. when I try to post the form...Nubilous
For your view, are you using erb and is your form using the <%= form_tag %> ?Goar
I am, and the expected CSRF content shows up in the form element and header when I view source.Nubilous
ApplicationController extends ActionController::API, so is your controller extending ApplicationController? If so try changing it to class MyController < ActionController::BaseGoar
Gave that a shot, same error. InvalidAuthenticityTokenNubilous
C
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

Coincide answered 23/2, 2021 at 18:4 Comment(0)
Z
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']
Zoe answered 15/3, 2017 at 8:11 Comment(3)
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.Nubilous
Are you both really sure that you do not need CSRF protection in API only with Ajax calls and cookies?Slam
How do you protect you APIs?Slam
H
1
class Api::ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
end

Use as above with rails 5

Hallock answered 18/1, 2019 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.