Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

400 Unable to verify your data submission on /freeform/payments/stripe/payment-intents request #1611

Closed
icreatestuff opened this issue Nov 7, 2024 · 2 comments
Assignees
Labels
issue Something isn't working correctly

Comments

@icreatestuff
Copy link

icreatestuff commented Nov 7, 2024

What happened?

We have a form that captures donations using the Stripe integration. The form is shown on every page of the website and on occasion, in multiple places on the same page, each form with its own unique ID and also implementing a custom fieldIdPrefix.

The pages are cached using the Blitz plugin and we have a script that runs an ajax request to get new CSRF, hash and payload data per form on page load.

forms.js

// Find the corresponding Form
const forms = document.querySelectorAll(".js-freeform");

forms.forEach((form, idx, array) => {
    // Refresh CSRF and Hash
    axios
        .get(`/ajax/freeform?form=${form.dataset.handle}`, {
            headers: { "X-Requested-With": "XMLHttpRequest" },
        })
        .then((response) => {
            // Update the Form Hash
            form.querySelector("input[name=formHash]").value = response.data.hash;

            // Update the Payload if encrypted payloads are enabled
            form.querySelector("input[name=freeform_payload]").value = response.data.payload;

            // If this is the last loop, update ALL
            // CSRF fields on the page with the same value
            if (idx === array.length - 1) {
                // Locate and update the CSRF input
                var csrf = response.data.csrf;
                
                document.querySelectorAll(`input[name=${csrf.name}]`).forEach((field) => {
                    field.value = csrf.value;
                });
            }
         });
 });

ajax/freeform.twig

{% set form = craft.freeform.form(craft.app.request.get('form')) %}
{{ {
    hash: form.hash,
    payload: form.payload,
    csrf: {
        name: craft.app.config.general.csrfTokenName,
        value: craft.app.request.csrfToken,
    }
}|json_encode|raw }}

This all works fine so far. The issue we notice occasionally is that when the form tries to set up the Stripe payment intent via an ajax request to /freeform/payments/stripe/payment-intents it fails with the 400 Bad Request - Unable to verify your data submission error, and for each of the donate forms on the page.

The user then sees a 'Could not load payment element' error in place of where the Stripe field should be.

Screenshot 2024-11-07 at 11 04 06

Could there be some sort of race condition occurring where the payment-intents ajax request is being sent using the old/cached CSRF if it gets triggered before the CSRF token is updated by the forms.js script? That might explain why it sometimes works ok and sometimes fails.

Errors and Stack Trace (if available)

No response

How can we reproduce this?

  1. Create a form to capture a donation using a custom amount field and Stripe payment field with 'Dynamic' payment amount type selected and payment type 'Single'. We also have the theme set to 'Default' and 'Layout' to 'Tabs', no floating labels.
  2. Include this form on multiple pages and on some pages multiple times.
  3. Cache the pages using the Blitz plugin
  4. Run a script on page load to refresh teh CSRF, hash and payload data for each form on the page
  5. Notice that on some pages the payment-intent ajax request fails.

Freeform Edition

Pro

Freeform Version

5.6.8

Craft Version

Pro 4.12.8

When did this issue start?

Unsure

Previous Freeform Version

No response

@icreatestuff icreatestuff added the issue Something isn't working correctly label Nov 7, 2024
@kjmartens
Copy link
Contributor

Sorry for the delay and the trouble you're experiencing @icreatestuff,

I will have a developer check into this and get back to you shortly. 🙂

@seandelaney
Copy link
Contributor

@icreatestuff

I'm struggling to replicate an issue.

The payment intent requests are set with no cache headers so Blitz should not be caching these.

I'd still add /freeform/* to Blitz exclude list though. Bad habit.

So the only reason I can see this request failing is a bad CSRF token value.

Personally I don't like how you've set the CSRF tokens. Whether you have 1 form on the page or 3 forms on the page, each form should be treated as unique and if you are refreshing a forms CSRF token, it should be done on a per form basis. I would not have made a request to get a fresh CSRF token and set its value on all forms. But thats me. But feel like this is the cause of the 400 bad request.

What I've done in the past:

{% js %}
    // Find the corresponding Form
    const forms = document.querySelectorAll("[data-freeform]");
    forms.forEach(form => {
        form.addEventListener("freeform-ready", function (event) {
            // Refresh CSRF and Hash
            fetch(`/ajax/freeform?form=${form.dataset.handle}`)
                .then(response => response.json())
                .then(response => {
                    // Update the Form Hash
                    if (response.hash) {
                        const hashInput = form.querySelector('input[name="formHash"]');
                        if (hashInput) {
                            hashInput.value = response.hash;
                        }
                    }

                    // Update the Payload
                    if (response.payload) {
                        const payloadInput = form.querySelector('input[name="freeform_payload"]');
                        if (payloadInput) {
                            payloadInput.value = response.payload;
                        }
                    }

                    // Update the CSRF
                    if (response.csrf) {
                        const csrfInput = form.querySelector(`input[name="${response.csrf.name}"]`);
                        if (csrfInput) {
                            csrfInput.value = response.csrf.value;
                        }
                    }
                });
        });
    });
{% endjs %}

Obviously I've used fetch in my code snippet. Feel feel to swap out again for Axios. I just didn't have this library at hand when testing. :)

Can you test my example snippet and let me know if it works better?

@solspace solspace locked and limited conversation to collaborators Nov 15, 2024
@kjmartens kjmartens converted this issue into discussion #1640 Nov 15, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
issue Something isn't working correctly
Development

No branches or pull requests

3 participants