rails - InvalidAuthenticityToken for json/xml requests - ruby-on-rails

For some reason I'm getting an InvalidAuthenticityToken when making post requests to my application when using json or xml. My understanding is that rails should require an authenticity token only for html or js requests, and thus I shouldn't be encountering this error. The only solution I've found thus far is disabling protect_from_forgery for any action I'd like to access through the API, but this isn't ideal for obvious reasons. Thoughts?
def create
respond_to do |format|
format.html
format.json{
render :json => Object.create(:user => #current_user, :foo => params[:foo], :bar => params[:bar])
}
format.xml{
render :xml => Object.create(:user => #current_user, :foo => params[:foo], :bar => params[:bar])
}
end
end
and this is what I get in the logs whenever I pass a request to the action:
Processing FooController#create to json (for 127.0.0.1 at 2009-08-07 11:52:33) [POST]
Parameters: {"foo"=>"1", "api_key"=>"44a895ca30e95a3206f961fcd56011d364dff78e", "bar"=>"202"}
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
thin (1.2.2) lib/thin/connection.rb:76:in `pre_process'
thin (1.2.2) lib/thin/connection.rb:74:in `catch'
thin (1.2.2) lib/thin/connection.rb:74:in `pre_process'
thin (1.2.2) lib/thin/connection.rb:57:in `process'
thin (1.2.2) lib/thin/connection.rb:42:in `receive_data'
eventmachine (0.12.8) lib/eventmachine.rb:242:in `run_machine'
eventmachine (0.12.8) lib/eventmachine.rb:242:in `run'
thin (1.2.2) lib/thin/backends/base.rb:57:in `start'
thin (1.2.2) lib/thin/server.rb:156:in `start'
thin (1.2.2) lib/thin/controllers/controller.rb:80:in `start'
thin (1.2.2) lib/thin/runner.rb:174:in `send'
thin (1.2.2) lib/thin/runner.rb:174:in `run_command'
thin (1.2.2) lib/thin/runner.rb:140:in `run!'
thin (1.2.2) bin/thin:6
/opt/local/bin/thin:19:in `load'
/opt/local/bin/thin:19

With protect_from_forgery enabled, Rails requires an authenticity token for any non-GET requests. Rails will automatically include the authenticity token in forms created with the form helpers or links created with the AJAX helpers--so in normal cases, you won't have to think about it.
If you're not using the built-in Rails form or AJAX helpers (maybe you're doing unobstrusive JS or using a JS MVC framework), you'll have to set the token yourself on the client side and send it along with your data when submitting a POST request. You'd put a line like this in the <head> of your layout:
<%= javascript_tag "window._token = '#{form_authenticity_token}'" %>
Then your AJAX function would post the token with your other data (example with jQuery):
$.post(url, {
id: theId,
authenticity_token: window._token
});

I had a similar situation and the problem was that I was not sending through the right content type headers - I was requesting text/json and I should have been requesting application/json.
I used curl the following to test my application (modify as necessary):
curl -H "Content-Type: application/json" -d '{"person": {"last_name": "Lambie","first_name": "Matthew"}}' -X POST http://localhost:3000/people.json -i
Or you can save the JSON to a local file and call curl like this:
curl -H "Content-Type: application/json" -v -d #person.json -X POST http://localhost:3000/people.json -i
When I changed the content type headers to the right application/json all my troubles went away and I no longer needed to disable forgery protection.

This is the same as #user1756254's answer but in Rails 5 you need to use a bit more different syntax:
protect_from_forgery unless: -> { request.format.json? }
Source: http://api.rubyonrails.org/v5.0/classes/ActionController/RequestForgeryProtection.html

Adding up to andymism's answer you can use this to apply the default inclusion of the TOKEN in every POST request:
$(document).ajaxSend(function(event, request, settings) {
if ( settings.type == 'POST' || settings.type == 'post') {
settings.data = (settings.data ? settings.data + "&" : "")
+ "authenticity_token=" + encodeURIComponent( window._token );
}
});

Since Rails 4.2, we have another way is to avoid verify_authenticity_token using skip_before_filter in your Rails App:
skip_before_action :verify_authenticity_token, only: [:action1, :action2]
This will let curl to do its job.
Ruby on Rails 4.2 Release Notes: https://guiarails.com.br/4_2_release_notes.html

As long as the JavaScript lives on the website served by Rails (for example: a JS snippet; or React app managed via webpacker) you can use the the value in csrf_meta_tags included in application.html.erb by default.
In application.html.erb:
<html>
<head>
...
<%= csrf_meta_tags %>
...
Therefore in the HTML of your website:
<html>
<head>
<meta name="csrf-token" content="XZY">
Grab the token from the content property and use it in the request:
const token = document.head.querySelector('meta[name="csrf-token"]').content
const response = await fetch("/entities/1", {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ authenticity_token: token, entity: { name: "new name" } })
});
This is similar to #andrewle's answer but there's no need for an additional token.

To add to Fernando's answer, if your controller responds to both json and html, you can use:
skip_before_filter :verify_authenticity_token, if: :json_request?

Related

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): While POSTing a JSON arugments to a Rails Controller

I am using a React/Redux frontend with a rails server running the backend. I have a button that onClick will send trigger an action that consists of two dispatches and a fetch, one dispatch before and one after. The fetch finds the server properly but I am given a 422 Error, meaning there is some issue on the Rails side of things after the request is accepted. The error is as you see in the title, ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken). However, I have the params set up to require a player object and permit the proper attributes.
The action with the fetch (which I know works) looks like this
export default function completeAttributeSelection(playerObj){
const playerPOST = ({
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
atk: playerObj.atk,
sAtk: playerObj.sAtk,
def: playerObj.def,
sDef: playerObj.sDef,
spd: playerObj.spd,
hp: playerObj.hp,
name: playerObj.name
})
})
return (dispatch) => {
dispatch({type: "LOADING"})
console.log("Domain: " + DOMAIN())
fetch((DOMAIN() + "/players/update_or_create"), playerPOST)
.then(resp => resp.json())
.then(json => {
console.log(json)
dispatch({type: "ATTRIBUTE_UPDATE_COMPLETE", payload: json})
})
}
}
And this is the controller that handles the request:
class PlayersController < ApplicationController
def update_or_create
puts ("Update or Create hit")
#player = Player.create_or_find_by(name: player_params[:name])
puts (player_params)
#player.update(class: player_params[:class], lvl: player_params[:level], atk: player_params[:atk], sAtk: player_params[:sAtk], def: player_params[:def], sDef: player_params[:sDef], spd: player_params[:spd], hp: player_params[:hp])
render json{#player}
end
private
def player_params
params.require(:player).permit(:name, :inv_hash, :lvl, :name, :class, :atk, :def, :sAtk, :sDef, :spd, :hp, :move_distance)
end
end
Since I am not using any secrets, keys, or anything like has_secure_password I am struggling to see what exactly is getting caught up by this.
The entirety of the prompts I get from the Rails Terminal (before the long jumbled error) is as follows...
Processing by PlayersController#update_or_create as JSON
Parameters: {"atk"=>6, "sAtk"=>6, "def"=>5, "sDef"=>9, "spd"=>10, "hp"=>85, "name"=>"test01", "player"=>{"name"=>"test01", "atk"=>6, "def"=>5, "sAtk"=>6, "sDef"=>9, "spd"=>10, "hp"=>85}}
HTTP Origin header (http://localhost:3000) didn't match request.base_url (http://localhost:3006)
Completed 422 Unprocessable Entity in 0ms (ActiveRecord: 0.3ms | Allocations: 394)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
The short answer to get this to work is adding protect_from_forgery with: :null_session to your controller.
class PlayersController < ApplicationController
protect_from_forgery with: :null_session
# ...
end
The longer answer deals with CSRF and the so called authenticity token. This is a good source it seems https://blog.nvisium.com/understanding-protectfromforgery
Cross-Site Request Forgery is a serious vulnerability that stems from
the trust that web applications place on the session identification
cookies that are being passed between browser and server. For a more
detailed explanation of CSRF, I suggest looking at the OWASP guide on
Cross-Site Request Forgery.
Rails includes a built-in mechanism for preventing CSRF,
protect_from_forgery, which is included by default in the
application_controller.rb controller when generating new applications.
This protect_from_forgery method leverages magic to ensure that your
application is protected from hackers!

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::UnknownFormat when uploading image with Dropzone

I have a new error regarding my Dropzone JS snippet. I use dropzone to upload images from an #Edit view.
One the images are created by another controller create action, I redirect to that very edit action.
This is where I get this new error :
Completed 406 Not Acceptable in 14ms (ActiveRecord: 1.1ms)
ActionController::UnknownFormat (PhotographesController#edit is missing a template for this request format and variant.
request.formats: ["application/json"]
request.variant: []):
actionpack (5.2.0) lib/action_controller/metal/implicit_render.rb:42:in `default_render'
actionpack (5.2.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `block in send_action'
actionpack (5.2.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `tap'
actionpack (5.2.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
actionpack (5.2.0) lib/abstract_controller/base.rb:194:in `process_action'
.....
It used to work perfectly in the past. But since I have set this snippet in place I have done quite a few things: installing gem "serviceworker-rails" and deferring the javascript..
I have undeferred the Javascript but the error is still here. Is it related to the serviceworker gem ?
EDIT
Dropzone snippet inside Photographes#edit view (inline)
<script>
// Dropzone = dynamic
var AUTH_TOKEN=$('meta[name="csrf-token"]').attr('content');
Dropzone.autoDiscover = false;
var myDropzone = new Dropzone("div#mydropzone",{
url: "<%= photographe_photographephotos_path(#photographe.hashed_id) %>",
autoProcessQueue: false,
autoDiscover: false,
uploadMultiple: true,
addRemoveLinks: true,
// clickable: false,
parallelUploads: 12,
maxFilesize: 5,
maxFiles: 12,
acceptedFiles: 'image/jpg, image/jpeg, image/png',
params:{
'authenticity_token': AUTH_TOKEN
},
successmultiple: function(data,response){
$('#msgBoard').append(response.message).addClass("alert alert-success");
$('#msgBoard').delay(2000).fadeOut();
$('#fileslist').val(response.filesList);
$('#photographedit').off('submit').submit();
}
});
$('#photographedit').submit(function(e){
if(myDropzone.getQueuedFiles().length > 0){
e.preventDefault();
myDropzone.processQueue();
}
});
</script>
Then Photographephotos#create
def create
#photographe = Photographe.find_by(hashed_id: params[:photographe_hashed_id])
if params[:file].present?
uploaded_pics = params[:file]
maximum_images=12
available_images = maximum_images - #photographe.photographephotos.count
n_keys = uploaded_pics.keys.first(available_images)
filtered_pics = uploaded_pics.slice(*n_keys)
filtered_pics.each do |index,pic|
#image = #photographe.photographephotos.new
#image.image = pic
#image.image_file_name = "Copyright" + #photographe.professionnel.first_name.to_s + #photographe.professionnel.last_name.to_s + ".JPG"
#image.save
end
end
redirect_to edit_photographe_path(#photographe.hashed_id)
end
The redirect at the end returns (should return) to the Photographes#edit view. What is funny if it used to work properly ...
I see that people used to have same problem with Jbuilder as per github. I have updated Jbuilder gem with no success.
Also the pictures are properly updated by paperclip. I get this error from logs when the redirection fails at the end of the images creation.
It seems like the controller is trying to render a template in a format that doesn't exist, maybe you can try specifying the format explicitly like this:
redirect_to edit_photographe_path(#photographe.hashed_id), :format => :html
This may happen if you received a request in a specific format and want to render a template in a different one.
I'm not sure if, in your case, the :format option should go inside the _path() or the redirect_to() method. If the above doesn't work try this:
redirect_to edit_photographe_path(#photographe.hashed_id, :format => :html)

Ajax POST can't seem to authenticate with Devise

Devise refuses to authenticate me when sending an ajax POST request.
What have I missed?
rails (4.1.6)
devise (3.4.1)
jquery-rails (2.3.0)
jquery-ui-rails (5.0.3)
Controller
class SeenEpisodesController < ApplicationController
before_filter :authenticate_user! #Devise
def mark_episode
#do stuff
respond_to do |format|
format.js
end
end
end
Javascript
Trying both with setting a header and a payload.
$.ajax({
type: "POST",
url: url,
dataType: "script",
data: {
authenticity_token: encodeURIComponent($('meta[name="csrf-token"]').attr('content'))
},
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
}
});
I have also tried without setting the tokens myself. jquery_ujs sets the header for me!
Yes I have <%= csrf_meta_tags %> in my layout.
Javascript manifest
//= require jquery
//= require jquery_ujs #Have tried with or without
Rails console output
Processing by SeenEpisodesController#mark_episode as JS
Parameters: {"mark"=>"1", "episode_id"=>"887795", "season_id"=>"65577"}
Completed 401 Unauthorized in 9ms
Started GET "/account/sign_in.js" for 192.168.0.10 at 2015-02-28 19:44:18 +0100
Processing by Devise::SessionsController#new as JS
Dev tools network output
%253D - try to decode this part of your authenticity_token. It will equal to =. So, seems like you have encoded authenticity_token incorrectly (twice).
You don't need to do encodeURIComponent when posting with jQuery. jQuery encodes the data for you anyway, which is leading to your data being encoded twice which will result in invalid authenticity token being submitted.

Get "ActionView::MissingTemplate" error, when I post json to rails controller from angular

I use angularjs in my new ruby on rails project. I create my view with angularjs and back end server with ruby on rails.
Now, I have a form in angular part, when user complete the form and click on send, I post a json like below to server:
$scope.jsonList = {
"form1": {name:"John",lastName:"Smart"},
"form2": {job:"student",age:"18"}
};
I post this json to a test action in rails controller. I put javascript (angularjs) and server side code below:
angularController:
//Services
.factory('Test', function($resource){
return $resource('/api/test.json', {}, {
create: { method: 'post'}
});
})
//Angular Controller
$scope.testResponse = Test.create(angular.toJson($scope.jsonList));
and routes.rb:
namespace :api, defaults: {format: :json} do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: :true) do
match '/test',to:'dropdowns#test', via: 'post'
end
end
and dropdownsController:
class Api::V1::DropdownsController < Api::V1::BaseController
def test
end
end
I do all this, but when I post json to rails server, I recieve json, but I get below error:
Started POST "/api/test.json" for 127.0.0.1 at 2014-11-22 17:11:10 +0330
Processing by Api::V1::DropdownsController#test as JSON
Parameters: {"form1"=>{"name"=>"John", "lastName"=>"Smart"}, "form2"=>{"job"=>"student", "age"=>"18"}, "dropdown"=>{"form1"=>{"name"=>"John", "lastName"=>"Smart"}, "form2"=>{"job"=>"student", "age"=>"18"}}}
Completed 500 Internal Server Error in 1ms
ActionView::MissingTemplate (Missing template api/v1/dropdowns/test, api/v1/base/test with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in:
* "/Users/mgh/Documents/Maliat/sama/app/views"
):
actionview (4.1.6) lib/action_view/path_set.rb:46:in `find'
actionview (4.1.6) lib/action_view/lookup_context.rb:124:in `find'
actionview (4.1.6) lib/action_view/renderer/abstract_renderer.rb:18:in `find_template'
actionview (4.1.6) lib/action_view/renderer/template_renderer.rb:41:in `determine_template'
I can do any thing with json when I recieve in rails controller (like parse and puts on console and save in database with other controller), but at the end of controller, I get above error and I cannot post any response to angular part. Where is the problem?
How can I fix this error?
You are not explicitly rendering anything in the test method so rails is looking for a template to send back as a response. However, you don't have a json template in your api/v1/dropdowns/ folder (I think it will look for a file called test.json in your case).
You can solve this by either sending back a response explicitly, (check out this guide for more about rendering a response.
def test
render plain: "This is the response from 'test' method in DropDownsController"
end
or you want to return json of a model
def test
render json: #product
end
OR
You can put a template in your api/v1/dropdowns/ folder and rails will serve that instead. But the overall point is you need to send back a response.
For example:
api/v1/dropdowns/test.json.erb
{
"name" : "my name is greg or something"
}

Resources