Rails CSRF issue on receiving POST request from API - ruby-on-rails

I am working on Rails application which interacts with the API. Now there is a step that the API will send the POST request to the application and I tried saving the returned data to the database. But it looks like the application does not accept the data and return 422 : Unprocessable Entity
Here is the error message from the log file
W, [2016-02-29T12:21:54.865291 #22727] WARN -- : Can't verify CSRF token authenticity
I, [2016-02-29T12:21:54.865839 #22727] INFO -- : Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
F, [2016-02-29T12:21:54.870852 #22727] FATAL -- :
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:181:in `handle_unverified_request'
actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:209:in `handle_unverified_request'
actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:204:in `verify_authenticity_token'
I also tried disabling the CSRF
class MyController < ApplicationController
skip_before_action :verify_authenticity_token
end
But it still doesn't work.
Please help me get through this. I have been struggling with this issue for three days.

You can try this
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }

Related

Rails Devise-omniauth route points to passthru instead of twitter

I am trying to implement omniauth-twitter with Devise in Ruby-on-Rails with no success.
According to the definitive article of Devise, the link
<%= link_to "Sign up with twitter", user_twitter_omniauth_authorize_path,
method: :post %>
should take the visitor to Twitter (Note I have disabled turbo site-wide with Turbo.session.drive = false and so turbo is irrelevant).
However, it just displays
Not found. Authentication passthru.
I notice the route looks wrong in the first place:
% bin/rails routes -g omni
Prefix Verb URI Pattern Controller#Action
user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format) users/omniauth_callbacks#passthru
user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format) users/omniauth_callbacks#twitter
It apparently points to Users::OmniauthCallbacksController#passthru, which is a non-existent method, hence the error?
This problem has been reported multiple times in Stackoverflow and Github (e.g., Github, SO1, SO2). A general advice seems to be using POST as opposed to GET. However, besides it is definitely POST in my case (as confirmed with a log file), I doubt if it is relevant to the routes!
What is the probable cause of this probblem and how can I solve it?
I confirm I am using OA1 API keys as opposed to Twitter OA2 (the latter is not supported by Omniauth, yet).. Also, as the doc of omniauth-twitter suggests, I confirm that "User authentication set up" is active in the Twitter Dev center to allow users to log in.
My relevant files are as follows:
# Gemfile
gem 'devise'
gem 'devise-i18n'
gem 'omniauth', '~> 2.1' #, '1.9.1'
gem 'omniauth-twitter'
gem 'omniauth-rails_csrf_protection'
# /config/initializers/devise.rb
Devise.setup do |config|
config.omniauth :twitter, 'MY_APP', 'MY_SECRET'
OmniAuth.config.logger = Rails.logger if Rails.env.development? # for debug
end
Note there are no Omniauth or Twitter-related configs in config/initializers/. Just devise.rb.
# /models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable, #...snipped...
:omniauthable, omniauth_providers: %i(twitter)
def self.from_omniauth(auth)
find_or_create_by(provider: auth.provider, uid: auth.uid) do |user|
user.display_name = auth.info.nickname.strip
user.skip_confirmation!
end
end
end
# /app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :verify_authenticity_token, only: [:twitter]
def twitter
callback_from __method__
end
def callback_from(provider)
# See User model
#user = User.from_omniauth(request.env['omniauth.auth'])
if #user.persisted?
sign_in_and_redirect #user, event: :authentication
set_flash_message(:notice, :success, kind: provider.to_s.capitalize) if is_navigational_format?
else
session["devise.#{provider.to_s}_data"] = request.env['omniauth.auth'].except(:extra)
redirect_to new_user_registration_url(from_omniauth_callback: true)
end
end
def failure
redirect_to root_path
end
end
# /config/routes.rb
Rails.application.routes.draw do
devise_for :users, except: [:destroy],
controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
end
View (the turbo-part is meaningless, for it is globally turned off).
<%# /app/views/devise/registrations/new.html.erb >
<%= link_to t(".sign_up_with_twitter"), user_twitter_omniauth_authorize_path,
method: :post, data: { turbo: false } %>
The standard output of the server (bin/dev in Rail 7):
00:... | I, [2022-...6 #793] INFO -- : Started POST "/en/users/auth/twitter" for 127.0.0.1 at 2022-11-27 00:33:14 +0000
00:... | I, [2022-...4 #793] INFO -- : Processing by Users::OmniauthCallbacksController#passthru as HTML
00:... | I, [2022-...0 #793] INFO -- : Parameters: {"authenticity_token"=>"[FILTERED]", "locale"=>"en"}
00:... | D, [2022-...0 #793] DEBUG -- : Rendering text template
00:... | I, [2022-...7 #793] INFO -- : Rendered text template (Duration: 0.0ms | Allocations: 10)
00:... | I, [2022-...9 #793] INFO -- : Completed 404 Not Found in 4ms (Views: 2.5ms | ActiveRecord: 0.0ms | Allocations: 1170)
Version information
omniauth-rails_csrf_protection (0.1.2) → (1.0.1)
Gemfile did not specify the version and yet a lower-version was installed. I now bundle install with '~> 1.0'. The same error still remains (after server-restart).
omniauth (2.1.0)
omniauth-oauth (1.2.0)
omniauth-twitter (1.4.0)
devise (4.8.1)
rails (7.0.4)
ruby (3.1.2)
That's it. Thank you.
This isn't a full answer, but more of a suggestion of where to keep looking.
You are supposed to see (twitter) Request phase initiated. in the logs immediately following Started POST "/users/auth/twitter" (from here).
But, the Omniauth controller is instead looking for an HTML template to render (and failing to find one).
INFO -- : Started POST "/en/users/auth/twitter" for 127.0.0.1 at 2022-11-27 00:33:14 +0000
INFO -- : Processing by Users::OmniauthCallbacksController#passthru as HTML
INFO -- : Parameters: {"authenticity_token"=>"[FILTERED]", "locale"=>"en"}
DEBUG -- : Rendering text template #<--- HERE!!!!
INFO -- : Rendered text template (Duration: 0.0ms | Allocations: 10)
INFO -- : Completed 404 Not Found in 4ms (Views: 2.5ms | ActiveRecord: 0.0ms | Allocations: 1170)
This does make sense; because Users::OmniauthCallbacksController#passthru is getting the POST request as HTML, it's looking for an HTML template to render (and failing to find one).
It seems like Omniauth expects an AJAX request as JSON, not an HTML request.
A few thoughts:
Turn Turbo back on and see what the content-type of the request is when Turbo gets to decide
Ditch the user_twitter_omniauth_authorize_path in favor of intercepting the link click and forming an AJAX POST request using Stimulus (Rails 7) or Javascript directly to the path /users/auth/twitter.
The language "en" in the URL of the POST is probably coming from some other gem (devise-i18n?) That could be interfering with the POST request somehow and turning the content-type to HTML instead of JSON.

Cannot figure out why I am getting random 'Can't verify CSRF token authenticity' errors on my rails controllers

The Problem
I have a multipage react application & rails backend that I am using as an API.
After a user logs out of the app, rails will throw CSRF errors when receiving any subsequent POST or DELETE requests until I perform a full page refresh in my browser.
e.g. After logging out, the login form POST request will cause 422 errors until I refresh the page. Then when logged in, post login POST / DELETE requests will randomly also trigger 422 errors until I refresh the page
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms | Allocations: 775)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
actionpack (6.0.3.4) lib/action_controller/metal/request_forgery_protection.rb:215:in `handle_unverified_request'
actionpack (6.0.3.4) lib/action_controller/metal/request_forgery_protection.rb:247:in `handle_unverified_request'
The CSRF token is being set in the application.html.erb file
<%= csrf_meta_tags %>
I have created an axios component to parse this token
const token = document.querySelector('[name="csrf-token"]') || {content: 'no-csrf-token'}
const axiosPost = axios.create({
headers: {
common: {
'X-CSRF-Token': token.content
}
}
})
export default axiosPost
And I am importing this in various other components & using it to make the requests e.g.
import AuthContext from './AuthContext'
const Logout = () => {
const {authState, setAuthState} = useContext(AuthContext)
const handleLogout = () => {
axiosPost.delete('/users/sign_out', {}, { withCredentials: true })
.then((resp) => {
setAuthState(false)
})
When the setAuthState value is set to false, my App.js component will re-render the page & display the Login component only.
Interesting, if I replace this logout / state driven re-render with
axiosPost.delete('/users/sign_out', {}, { withCredentials: true })
.then((resp) => {
setAuthState(false)
window.location.href = '/login'
})
It triggers a page refresh and I don't get the CSRF error on the backend (at least not as frequently).
I am starting to think that the CSRF token in the layout needs to be reloaded / refreshed after a user logs out of the application but maybe this is just another red herring.
One other thing, I did overwrite the destroy method in the devise sessions controller with
def destroy
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
if signed_out
head :no_content
end
end
I don't see how this method would cause the CSRF issue, but adding it for additional context.
Application Stack
Rails 6.0.3.4 on the backend (full stack, not just api)
CSRF protection enabled
Devise for user authentication & to authorize access to protected
controllers
React on the front-end.
My react app is initialized once through application.html.erb
Any help / guidance is appreciated

"ActionController::InvalidAuthenticityToken" error when using form_with

I have a form that looks like this:
<%= form_with(url: star.starname, method: :post, local: true) do |f| %>
<% star.availabilities.each do |avail| %>
<%= f.label avail.time_slot %>
<%= radio_button_tag(:time_slot, avail.time_slot) %> <br>
<% end %>
<%= f.submit "Create" %>
<% end %>
Immediately after form submission:
Notes:
This is occurring in an app (not an API), so sessions are important, hence CSRF protection must be left on.
The problem occurs in chrome, incognito, and safari.
I have tried logging in with different users and clearing cookies (in case it was being caused by a stale token)
Some more of the error message:
Started POST "/talljohn" for ::1 at 2020-09-16 10:06:21 +1000
Processing by StarsController#book as HTML
Parameters: {"authenticity_token"=>"P++4a+giwUBqZgCLfMwqKpMu0EGitd8zTOi5RWsnxpKlNcjiuU6hd3ebbIC/IOxlL74RJIvrq+yDuA1ZtfcvFw==", "time_slot"=>"2020-09-16 01:00:00 UTC", "commit"=>"Create", "starname"=>"talljohn"}
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms | Allocations: 655)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
actionpack (6.0.3.2) lib/action_controller/metal/request_forgery_protection.rb:215:in `handle_unverified_request'
actionpack (6.0.3.2) lib/action_controller/metal/request_forgery_protection.rb:247:in `handle_unverified_request'
devise (4.7.2) lib/devise/controllers/helpers.rb:255:in `handle_unverified_request'
actionpack (6.0.3.2) lib/action_controller/metal/request_forgery_protection.rb:242:in `verify_authenticity_token'
activesupport (6.0.3.2) lib/active_support/callbacks.rb:428:in `block in make_lambda'
activesupport (6.0.3.2) lib/active_support/callbacks.rb:200:in `block (2 levels) in halting'
actionpack (6.0.3.2) lib/abstract_controller/callbacks.rb:34:in `block (2 levels) in <module:Callbacks>'
activesupport (6.0.3.2) lib/active_support/callbacks.rb:201:in `block in halting'
Update
I reverted back to the last working version of the form, which was exactly the same as above but without , local: true. Then it suddenly works! (no errors).
I thought local: true (or remote: false) simply turns off ajax form submission. So I don't understand why that would make any difference (or have anything to do with CSRF), it seems that those two aspects are unrelated and it isn't clear why these two concepts would have any affect on eachother
Update 2
I later realised that another untouched previously working form also produced this error. It had not been changed in any way. I tried it in chrome incognito, and it produced the error. Half an hour later (without changing any code) I tried it again in the same browser and it worked. This (very) strange behaviour makes me think it's something to do with sessions, cookies or caching. I will report back if I learn anything further
Update 3
After reading Sarah's solution adding protect_from_forgery prepend: true to the application controller (I tried both before and after before_action :authenticate_user!), the same error message appears in the logs, the POST request isn't actioned, but the app redirects to the home page. I.e. upon POST I see:
Can't verify CSRF token authenticity.
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms | Allocations: 444)
Started GET "/users/sign_in" for ::1 at 2020-09-17 21:08:42 +1000
Processing by Devise::SessionsController#new as HTML
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
Redirected to http://localhost:3000/
Filter chain halted as :require_no_authentication rendered or redirected
Completed 302 Found in 3ms (ActiveRecord: 0.5ms | Allocations: 1900)
Update 4
I attempted to manually clear the rails fragment cache (with Rails.cache.clear ). But the result is exactly the same before/after clearing the fragment cache.
I remember running into something like this and adding protect_from_forgery prepend: true before any user authentication to the ApplicationController solved it.
TL;DR: after 2 weeks' of debugging attempts, I turned off turbolinks and the problem went away.
Aside from turning off turbolinks, another solution appears to be (mentioned here) adding this to application.js
$(document).on('turbolinks:load', function(){ $.rails.refreshCSRFTokens(); });
Previous answer
The issue kept reemerging. I have tried the following six things but it still hasn't fixed it
1. Clear the fragment cache
Rails.cache.clear (warning because it clears the cache, it will also remove things like sidekiq jobs etc). This will remove the stale token and refreshing the app in the browser will return things to normal, and the form should submit (a simple 'resubmit' won't work, so go back to the form page, refresh, then submit and it should work)
2. Hard refresh page
Press cmd + opt + j to bring up the developer console, then right click on refresh and select 'Empty Cache and Hard Reload'
3. Delete site cookies
Right click on the tiny icon to the immediate left of the url (it will be a lock if using https, or the letter 'i' if using http). Go into each of categories listed (e.g. 'Cookies', 'Site Settings' etc) and delete them all
4. Delete cookies for other urls that point to the same site
For example, if your site is www.example.com, and it's hosted on heroku at www.example.herokuapp.com, then delete cookies for that second url as well
5. Delete cookies for localhost
I deleted localhost cookies just to be sure
6. Testing in completely isolated instances of chrome
For me, the solution was to switch to a more specific Cookie Strategy:
# config/initializers/new_framework_defaults_6_1.rb
# Before: i've used None, but this leads to broken Cookies somehow. Strict or Lax seems to work.
Rails.application.config.action_dispatch.cookies_same_site_protection = :strict

Rails 4 ActionController::InvalidAuthenticityToken with multiple subdomains

Before let me say that I search google and a lot of posts here at Stackoverflow, but nothing was able to give a solution for my problem. for this, I am creating a new post.
I am creating an application with two sub-domains: account.psantos.dev and app.psantos.dev.
At: config/initializers/session_store.rbI have:
Rails.application.config.session_store :cookie_store, key: '_psantos.co.ao_session', domain: 'psantos.dev'
And I have the following structure folders:
- app
-- controllers
--- app1
---- welcome_controller.rb
--- account
---- welcome_controller.rb
--views
--- layout
---- app1.html.erb
---- account.html.erb
--- app1
---- welcome
----- index.html.erb
--- account
---- welcome
----- index.html.erb
at: app/views/layout/app1.html.erb (http://app1.psantos.dev) I have the following line:
<li><%= "Logout", account_sign_out_url, method: :delete %></li>
When I click this Link (that will go to: http://account.psantos.dev), I got the following error:
ActionController::InvalidAuthenticityToken at /sign_out
ActionController::InvalidAuthenticityToken
on both layout files (app1.html.erb and account.html.erb) I have this line before <\head > tag :
<%= csrf_meta_tags %>
how can I solve this problem?
Update: relevant log
Started GET "/" for 127.0.0.1 at 2015-08-08 12:37:03 +0100 Processing
by APP1::WelcomeController#index as HTML Parameters:
{"subdomain"=>"app1"} Rendered app1/welcome/index.html.erb within
layouts/app1 (0.4ms) [1m[35mEntity Load (0.3ms)[0m SELECT
"entities".* FROM "entities" WHERE "entities"."user_token" = $1 LIMIT
1 [["user_token", "xxxxxxxxxxxxxxxxxxxx"]] Completed 200 OK in 43ms
(Views: 42.0ms | ActiveRecord: 0.3ms)
Started DELETE "/sign_out" for 127.0.0.1 at 2015-08-08 12:37:05 +0100
Processing by Account::SessionsController#destroy as HTML
Parameters: {"subdomain"=>"account"} Can't verify CSRF token
authenticity Completed 422 Unprocessable Entity in 1ms (ActiveRecord:
0.0ms)
ActionController::InvalidAuthenticityToken -
ActionController::InvalidAuthenticityToken: actionpack (4.2.3)
lib/action_controller/metal/request_forgery_protection.rb:181:in
`handle_unverified_request' actionpack (4.2.3)
lib/action_controller/metal/request_forgery_protection.rb:209:in
`handle_unverified_request' actionpack (4.2.3)
lib/action_controller/metal/request_forgery_protection.rb:204:in
`verify_authenticity_token' activesupport (4.2.3)
lib/active_support/callbacks.rb:430:in `block in make_lambda'
activesupport (4.2.3) lib/active_support/callbacks.rb:143:in `block in
halting_and_conditional' activesupport (4.2.3)
lib/active_support/callbacks.rb:502:in `block in call' activesupport
(4.2.3) lib/active_support/callbacks.rb:502:in `call'
Now it's impossible to do :delete request to subdomain.
There is an vulnerability in jquery-ujs and jquery-rails that can be used to bypass CSP protections and allows attackers to send CSRF tokens to attacker domains. This vulnerability has been assigned the CVE identifier CVE-2015-1840. Versions Affected: All. Not affected: Applications which don't use jquery-ujs or jquery-rails. Fixed Versions: jquery-rails versions 4.0.4 and 3.1.3 and jquery-ujs 1.0.4. Impact ------ In the scenario where an attacker might be able to control the href attribute of an anchor tag or the action attribute of a form tag that will trigger a POST action, the attacker can set the href or action to " https://attacker.com" (note the leading space) that will be passed to JQuery, who will see this as a same origin request, and send the user's CSRF token to the attacker domain.
This is commit to jquery-ujs:
You can read more here

Webhook returning 401 in Rails

I have created a webhook using Fluidsurveys which corresponds to this controller:
class HooksController < ApplicationController
skip_before_filter :verify_authenticity_token
def response_created_callback
# If the body contains the score parameter...
if params[:_completed].present?
# Create a new Response object based on the received parameters...
response = FluidsurveysResponse.new(:completed => params[:_completed])
response.invite_email = params[:_invite_email]
response.locale = params[:_locale]
response.key = params[:_key]
response.webhook = params[:webhook]
response.survey_name = params[:survey_name]
response.username = params[:_username]
response.weighted_score = params[:_weighted_score]
response.completion_time = params[:_completion_time]
response.ip_address = params[:_ip_address]
response.num_responses = params[:num_responses]
response.survey_id = params[:survey_id]
response.invite_name = params[:_invite_name]
response.language = params[:_language]
response.referrer = params[:_referrer]
response.fs_created_at = params[:_created_at]
response.fs_updated_at = params[:_updated_at]
response.survey_url = params[:survey_url]
response.fs_id = params[:_id]
response.collector_id = params[:_collector_id]
response.comment = params[:Comment]
response.nps_score = params[:NPSScore]
response.save!
end
# The webhook doesn't require a response but let's make sure
# we don't send anything
render :nothing => true
end
end
The webhook seems to work fine, as I see this in my logs:
2014-01-20T13:49:01.231772+00:00 app[web.1]: Started POST "/hooks/response_created_callback" for 184.107.171.218 at 2014-01-20 13:49:01 +0000
2014-01-20T13:49:01.327989+00:00 app[web.1]: Processing by HooksController#response_created_callback as */*
2014-01-20T13:49:01.328149+00:00 app[web.1]: Parameters: {"_invite_email"=>"N/A", "_locale"=>"298", "_updated_at"=>"2014-01-20 13:49:00.738850", "_language"=>"en", "_key"=>"b5ec09680a4274cec0052d4049bec338a906e5b8", "webhook"=>"event", "survey_name"=>"Test", "_referrer"=>"http://fluidsurveys.com/surveys/marketing/test-nps-for-dc/", "_username"=>"marketing", "survey_url"=>"http://fluidsurveys.com/surveys/marketing/test-nps-for-dc/", "_id"=>"39164209", "_created_at"=>"2014-01-20 13:49:00.243640", "_weighted_score"=>"0.0", "_completion_time"=>"00:00:00", "_completed"=>"1", "_ip_address"=>"66.192.31.1", "yptmAoJw6i"=>"Detractor: 0", "num_responses"=>"261", "survey_id"=>"263692", "_extra_info"=>"weighted_score", "_invite_name"=>"N/A"}
2014-01-20T13:49:01.369963+00:00 app[web.1]: Completed 401 Unauthorized in 41ms
As you can see, the post gets made to the right controller action. The route is setup correctly, but I get a 401 error. I don't understand what authentication needs to happen here...The params are getting passed to the controller, and in my mind I see no need for my app to authenticate anything. It receives a request, does what it's told, and then it's done.
What am I missing here that is causing the 401 error?
EDIT:
After remove skip_before_filter :verify_authenticity_token from my controller, I get this error:
Can't verify CSRF token authenticity
2014-01-20T16:07:12.222512+00:00 app[web.1]: Completed 422 Unprocessable Entity in 28ms
2014-01-20T16:07:12.225905+00:00 app[web.1]:
2014-01-20T16:07:12.225905+00:00 app[web.1]: ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
The issue is that I am using devise for user authentication, and my app was blocking the post request because there were no authentication credentials being passed to my controller.
By adding this line to my controller I solved the issue:
skip_before_filter :verify_authenticity_token, :authenticate_user!

Resources