Search code examples
ruby-on-railsformsauthenticationstripe-paymentscsrf

Including Stripe card info in ajax form removes authenticity_token


I have a form_tag using Ajax to save a new user card with Stripe. It renders a Stripe card form, passes the input to a controller method, and then is supposed to serve a js file. It works without the Stripe stuff, but with it, I'm getting authentication issues.

Here's the basic code:

<%= form_tag(save_card_path, id:'payment-form', remote: true) do %>
    <div id="card-element">
      <!-- A Stripe Element will be inserted here. -->
    </div>
  <button id="submit-card" class="submit-btn">Save Card</button>
<% end %>

<script type="text/javascript">

  var stripe = Stripe('<%= @stripe_public %>');
  var elements = stripe.elements();

  // Custom styling can be passed to options when creating an Element.
  var style = {
    base: {
      // Add your base input styles here. For example:
      fontSize: '20px',
      color: "#32325d",
    }
  };

  // Create an instance of the card Element.
  var card = elements.create('card', {style: style});

  // Add an instance of the card Element into the `card-element` <div>.
  card.mount('#card-element');

  card.addEventListener('change', function(event) {
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      displayError.textContent = event.error.message;
    } else {
      displayError.textContent = '';
    }
  });

  // Create a token or display an error when the form is submitted.
  var form = document.getElementById('payment-form');
  form.addEventListener('submit', function(event) {
    event.preventDefault();

    stripe.createToken(card).then(function(result) {
      if (result.error) {
        // Inform the customer that there was an error.
        var errorElement = document.getElementById('card-errors');
        errorElement.textContent = result.error.message;
      } else {
        // Send the token to your server.
        stripeTokenHandler(result.token);
      }
    });
  });

  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    form.appendChild(hiddenInput);

    // Submit the form
    form.submit();
  }
</script>

card_controller.rb

def save_card
  respond_to do |format|
    format.js
  end
end

save_card.js.erb

$("html").hide();

So like I said, without the Stripe code everything works fine, and an authenticity_token is present in the params, but the code as I wrote it above gives the following error:

    def handle_unverified_request
      raise ActionController::InvalidAuthenticityToken
    end

with only the params:

{"utf8"=>"✓", "stripeToken"=>"<token>"}

When I add the option authenticity_token: true to the form, the params once again contain an authenticity_token, but now when it gets to the format.js line, I get the error

ActionController::UnknownFormat

I've run into a similar problem before while trying to upload files in forms via Ajax, but I discovered the remotipart gem, and that solved it. But it doesn't appear to help in this case.

Does anyone know why including a Stripe field would get rid of my authenticity_token, and why even with an authenticity_token, the js format isn't recognized?


EDIT:

Jquery-ujs is included via the line //= require jquery_ujs in my application.js, the line <%= csrf_meta_tags %> is included in application.html.erb, and my source code includes the lines

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<TOKEN>" />

This is the case whether my ajax call works or not.

UPDATE:

This is my full log when I add the option authenticity_token: true:

Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:07:04 +0100

Processing by UsersController#save_card as HTML

Parameters: {"utf8"=>"✓", "authenticity_token"=>"[LONG TOKEN]", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"}

User Load (0.9ms)

SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 2], ["LIMIT", 1]]

(0.3ms) BEGIN SQL

(2.3ms) INSERT INTO "cards" ("stripe_customer_id", "brand", "last4", "exp_month", "exp_year", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["stripe_customer_id", "[STRIPE TOKEN]"], ["brand", "Visa"], ["last4", "4242"], ["exp_month", "4"], ["exp_year", "2024"], ["user_id", 2], ["created_at", "2020-02-19 23:07:05.246789"], ["updated_at", "2020-02-19 23:07:05.246789"]]

(2.8ms) COMMIT

Completed 406 Not Acceptable in 672ms (ActiveRecord: 6.3ms)

ActionController::UnknownFormat (ActionController::UnknownFormat): app/controllers/users_controller.rb:485:in `save_card' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:07:05 +0100 Started GET "/serviceworker.js" for ::1 at 2020-02-20 00:07:21 +0100

and this is my log when I remove authenticity_token: true:

Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:33:17 +0100

Processing by UsersController#save_card as HTML

Parameters: {"utf8"=>"✓", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"} Can't verify CSRF token authenticity. Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:195:in handle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:227:in handle_unverified_request' devise (4.7.0) lib/devise/controllers/helpers.rb:255:in handle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:222:in verify_authenticity_token' activesupport (5.1.7) lib/active_support/callbacks.rb:413:in block in make_lambda' activesupport (5.1.7) lib/active_support/callbacks.rb:197:inblock (2 levels) in halting' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:12:in block (2 levels) in <module:Callbacks>' activesupport (5.1.7) lib/active_support/callbacks.rb:198:inblock in halting' activesupport (5.1.7) lib/active_support/callbacks.rb:507:in block in invoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:507:ineach' activesupport (5.1.7) lib/active_support/callbacks.rb:507:in invoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:130:inrun_callbacks' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:19:in process_action' actionpack (5.1.7) lib/action_controller/metal/rescue.rb:20:inprocess_action' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:32:in block in process_action' activesupport (5.1.7) lib/active_support/notifications.rb:166:inblock in instrument' activesupport (5.1.7) lib/active_support/notifications/instrumenter.rb:21:in instrument' activesupport (5.1.7) lib/active_support/notifications.rb:166:in instrument' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:30:in process_action' actionpack (5.1.7) lib/action_controller/metal/params_wrapper.rb:252:inprocess_action' activerecord (5.1.7) lib/active_record/railties/controller_runtime.rb:22:in process_action' actionpack (5.1.7) lib/abstract_controller/base.rb:124:inprocess' actionview (5.1.7) lib/action_view/rendering.rb:30:in process' actionpack (5.1.7) lib/action_controller/metal.rb:189:indispatch' actionpack (5.1.7) lib/action_controller/metal.rb:253:in dispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:49:indispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:31:in serve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:50:inblock in serve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:in each' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:inserve' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:844:in call' serviceworker-rails (0.6.0) lib/serviceworker/middleware.rb:35:in call' remotipart (1.4.3) lib/remotipart/middleware.rb:32:in call' warden (1.2.8) lib/warden/manager.rb:36:inblock in call' warden (1.2.8) lib/warden/manager.rb:34:in catch' warden (1.2.8) lib/warden/manager.rb:34:incall' rack (2.0.7) lib/rack/etag.rb:25:in call' rack (2.0.7) lib/rack/conditional_get.rb:38:incall' rack (2.0.7) lib/rack/head.rb:12:in call' rack (2.0.7) lib/rack/session/abstract/id.rb:232:incontext' rack (2.0.7) lib/rack/session/abstract/id.rb:226:in call' actionpack (5.1.7) lib/action_dispatch/middleware/cookies.rb:613:incall' activerecord (5.1.7) lib/active_record/migration.rb:556:in call' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:26:inblock in call' activesupport (5.1.7) lib/active_support/callbacks.rb:97:in run_callbacks' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:24:incall' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:in call' actionpack (5.1.7) lib/action_dispatch/middleware/debug_exceptions.rb:59:incall' web-console (3.7.0) lib/web_console/middleware.rb:135:in call_app' web-console (3.7.0) lib/web_console/middleware.rb:30:inblock in call' web-console (3.7.0) lib/web_console/middleware.rb:20:in catch' web-console (3.7.0) lib/web_console/middleware.rb:20:incall' actionpack (5.1.7) lib/action_dispatch/middleware/show_exceptions.rb:31:in call' railties (5.1.7) lib/rails/rack/logger.rb:36:incall_app' railties (5.1.7) lib/rails/rack/logger.rb:24:in block in call' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:inblock in tagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:26:in tagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:intagged' railties (5.1.7) lib/rails/rack/logger.rb:24:in call' sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:13:incall' actionpack (5.1.7) lib/action_dispatch/middleware/remote_ip.rb:79:in call' actionpack (5.1.7) lib/action_dispatch/middleware/request_id.rb:25:incall' rack (2.0.7) lib/rack/method_override.rb:22:in call' rack (2.0.7) lib/rack/runtime.rb:22:incall' activesupport (5.1.7) lib/active_support/cache/strategy/local_cache_middleware.rb:27:in call' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:incall' actionpack (5.1.7) lib/action_dispatch/middleware/static.rb:125:in call' rack (2.0.7) lib/rack/sendfile.rb:111:incall' railties (5.1.7) lib/rails/engine.rb:522:in call' puma (3.12.1) lib/puma/configuration.rb:227:incall' puma (3.12.1) lib/puma/server.rb:660:in handle_request' puma (3.12.1) lib/puma/server.rb:474:inprocess_client' puma (3.12.1) lib/puma/server.rb:334:in block in run' puma (3.12.1) lib/puma/thread_pool.rb:135:inblock in spawn_thread' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:33:18 +0100


Solution

  • Assuming you are on Rails 5.X and you have //= require rails-ujs in your application.js file

    Instead of form.submit(), using Rails.fire(form, 'submit') will allow you the submit the form via Ajax