34

I'm trying to give users on my website "points" or "credits" for tweeting about out the brand name.

I have the fancy twitter widget on the appropriate view...

<p><a  href="https://twitter.com/share" class="twitter-share-button" data-text="Check Out This Awesome Website Yay" data-via="BrandName" data-hashtags="ProductName">Tweet</a>
<div id="credited"></div>
<script>window.twttr = (function (d, s, id) {
  var t, js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src= "https://platform.twitter.com/widgets.js";
  fjs.parentNode.insertBefore(js, fjs);
  return window.twttr || (t = { _e: [], ready: function (f) { t._e.push(f) } });
}(document, "script", "twitter-wjs"));
</script>    

I have the JS all written up and pretty....

function creditTweet() {
  $.post(
    "/credit_tweet",
    {},
    function(result) {
      var text;
      if (result.status === "noop") {
        text = "Thanks for sharing already!";
      } else if (result.status === "ok") {
        text = "5 Kredit Added";
      }
      $("#credited").html(text);
    }
  );
}

$(function() {
  twttr.ready(function (twttr) {
    window.twttr.events.bind('tweet', creditTweet);
  }); 
});

Now the problem is either in the controller OR in the routes (where I'm posting). I think the routes are fine because the POST is almost working, because this is the description of the error on wikipedia - "422 Unprocessable Entity (WebDAV; RFC 4918) The request was well-formed but was unable to be followed due to semantic errors."

So, do you guys see anything wrong with my ruby code in the controller?

class SocialKreditController < ApplicationController
    TWEET_CREDIT_AMOUNT = 5

  def credit_tweet
    if !signed_in?
      render json: { status: :error }
    elsif   current_user.tweet_credited
        Rails.logger.info "Not crediting #{ current_user.id }"
        render json: { status: :noop }
      else
        Rails.logger.info "Crediting #{ current_user.id }"
        current_user.update_attributes tweet_credited: true
        current_user.add_points TWEET_CREDIT_AMOUNT
        render json: { status: :ok }
      end
  end
end

And in my routes.rb, it's pretty straight forward, so I doubt there's anything wrong here...

  get 'social_kredit/credit_tweet'
  post '/credit_tweet' => 'social_kredit#credit_tweet'

Where oh where is this error? I clearly don't know smack about HTTP requests.

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
piratetone
  • 1,309
  • 1
  • 10
  • 13
  • IME rails usually returns 422 when an ActiveRecord object fails to update/save. What does your log say? – ihaztehcodez Nov 24 '14 at 05:30
  • Looks like .... Started POST "/credit_tweet" for 127.0.0.1 at 2014-11-24 00:34:53 -0500 Processing by SocialKreditController#credit_tweet as */* Can't verify CSRF token authenticity Completed 422 Unprocessable Entity in 1ms ActionController::InvalidAuthenticityToken - ActionController::InvalidAuthenticityToken: – piratetone Nov 24 '14 at 05:35
  • can you post the log from rails server with lines which show this error? Also, why you have two routes `get` and `post` pointing to same method? – Surya Nov 24 '14 at 05:45

6 Answers6

75

I got it working!

I added a...

skip_before_action :verify_authenticity_token

to the controller.

The issue was found when checking out the logs and seeing that the CSRF token could not be verified.

piratetone
  • 1,309
  • 1
  • 10
  • 13
  • 15
    You may also want to check out [this question](http://stackoverflow.com/questions/7203304/warning-cant-verify-csrf-token-authenticity-rails) for a solution which doesn't require disabling the csrf token check. – ihaztehcodez Nov 24 '14 at 05:52
  • thanks @ihaztehcodez - this skip_before_action :verify_authenticity_token is only used on a controller that is necessary for users after being signed in. Are there any major reasons why I may want to use the AJax solution provided in that link over this little snippet? – piratetone Nov 24 '14 at 06:11
  • 14
    @piratetone : Yes, that reason is security. – Surya Nov 24 '14 at 06:15
  • 6
    [The rails security guide](http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) does a good job of explaining csrf. In general, you should not disable the csrf check without a good reason (callbacks from a third-party API for instance). In this case it is completely within your power to make the csrf check happy and that is the best thing to do. – ihaztehcodez Nov 24 '14 at 06:34
  • This is the solution! – jshaf Apr 05 '17 at 23:39
  • 2
    Turning off security is the solution? – bradw2k Mar 04 '20 at 22:17
  • Tokens for everyone yei! – Erick Mar 26 '23 at 01:00
6

ihaztehcodez(who was last active in 2016 so it won't help nudging him to post an answer) mentions that the skip_before_action :verify_authenticity_token technique is not so secure 'cos you lose forgery protection.

they mention that the best/secure/'better practise', solutions are mentioned here WARNING: Can't verify CSRF token authenticity rails

e.g.

$.ajaxSetup({
  headers: {
    'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
  }
});

or

$.ajax({ url: 'YOUR URL HERE',
  type: 'POST',
  beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
  data: 'someData=' + someData,
  success: function(response) {
    $('#someDiv').html(response);
  }
});

or

putting this within an ajax request

headers: {
  'X-Transaction': 'POST Example',
  'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
barlop
  • 12,887
  • 8
  • 80
  • 109
2

Same problem I faced. It sorts out after adding

skip_before_action :verify_authenticity_token

at the top of your controller where your JS is calling or sending data.

class UserController < ApplicationController
    skip_before_action :verify_authenticity_token
    def create
    end
end

as shown in code snippet.

Shekhar Patil
  • 337
  • 4
  • 14
1

I had this challenge when working on a Rails 6 API-only application.

I followed the answer here - Rails: How to implement protect_from_forgery in Rails API mode to implement protect_from_forgery in Rails API mode, but I was still having the Can't verify CSRF token authenticity error followed by the 422 Unprocessable Entity error when I send post requests from Postman:

Started POST "/api/v1/programs" for ::1 at 2021-02-23 18:42:49 +0100
Processing by Api::V1::ProgramsController#create as JSON
  Parameters: {"program"=>{"name"=>"Undergraduate", "code"=>"UD", "affiliate_status"=>false, "motto"=>"Our motto is ...", "description"=>"This is a new program", "school_id"=>1}}
Can't verify CSRF token authenticity.
  TRANSACTION (0.3ms)  BEGIN
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
  Baserecord::School Load (0.3ms)  SELECT "schools".* FROM "schools" WHERE "schools"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
  TRANSACTION (0.4ms)  ROLLBACK
  ↳ app/controllers/api/v1/programs_controller.rb:27:in `create'
Completed 422 Unprocessable Entity in 30ms (Views: 0.8ms | ActiveRecord: 6.9ms | Allocations: 13172)

Here's I solved it:

The issue was caused by a validation error from my models. A validation I added to the model for the controller I was calling was failing. I had to check the Body of the response returned by Postman after making the request to find the error:

{
    "affiliate_status": [
        "can't be blank"
    ]
}

Once I fixed the error following this answer - Rails: Validation fails for ActiveRecord in setting a random Boolean Attribute, everything worked fine afterward.

That's all.

I hope this helps

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
1

a 422 can happen when updating a model in a POST request:

e.g. @user.update!(email: nil) in users#update will make a 422 if there is a validation on email like validates :email, presence: true

Dorian
  • 7,749
  • 4
  • 38
  • 57
0

If you're including Rails meta data in the HTML header with <%= csrf_meta_tags %> it'll generate the following.

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="ihwlaOLL232ipKmWYaqbSZacpJegQqooJ+Cj9fLF2e02NTQw7P/MfQyRuzruCax2xYWtEHWsb/uqiiZP6NWH+Q==" />

You can pull the CRSF token from the meta data and pass it into your async request. Using the native js fetch method you can pass it in as a x-csrf-token header.

This is a trimmed onSave handler for a React component that enhances a standard Rails form.

  onSaveHandler = (event) => {
    const data = "Foo Bar";
    const metaCsrf = document.querySelector("meta[name='csrf-token']");
    const csrfToken = metaCsrf.getAttribute('content');
    fetch(`/posts/${this.props.post_id}`, {
      method: "PUT",
      body: JSON.stringify({
        content: data
      }),
      headers: {
        'x-csrf-token': csrfToken,
        'content-type': 'application/json',
        'accept': 'application/json'
      },
    }).then(res => {
      console.log("Request complete! response:", res);
    });
  }

Forgery protection is a good idea. This way we stay secure and don't mess with our Rails configuration.

Using gem 'rails', '~> 5.0.5' & "react": "^16.8.6",

Lex
  • 4,749
  • 3
  • 45
  • 66