Reputation: 4511
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.
Upvotes: 40
Views: 36178
Reputation: 28920
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
Upvotes: 7
Reputation: 209
class Api::ApiController < ApplicationController
skip_before_action :verify_authenticity_token
end
Use as above with rails 5
Upvotes: 2
Reputation: 4511
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.
Upvotes: 38
Reputation: 4427
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']
Upvotes: 3
Reputation: 654
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
Upvotes: 23