craft-freeform icon indicating copy to clipboard operation
craft-freeform copied to clipboard

fix(SFT-2077): Replaced Crafts dynamic CSRF input with a custom CSRF input

Open seandelaney opened this issue 7 months ago • 2 comments

Fixes https://github.com/solspace/craft-freeform/issues/1980

After deeper investigation, the asyncCsrfInput config setting does not render correctly in our multipage handling and using Html::csrfInput()

On page 1, it seems to render, meaning <craft-csrf-input></craft-csrf-input> gets converted into <input type="hidden" name="CSRFTokenName" value="CSRFTokenValue" /> as expected.

On pages 2+, <craft-csrf-input></craft-csrf-input> is again injected into the page but it does not get converted into <input type="hidden" name="CSRFTokenName" value="CSRFTokenValue" />. The JS used by Craft to find and replace the HTML tag, fails.

A workaround for Freeform is to just roll our own CSRF input, whether asyncCsrfInput config setting enabled or not. Rolling our own does not affect how Craft handles asyncCsrfInput for other non Freeform forms on the page or other security usages. This change just tells Freeform to always use our own approach. To get CSRF token name and value and inject into the form each time the form is rendered.

I set asyncCsrfInputs to be true in my Craft config.

I've tested a multiple page form with Stripe and all worked as expected.

Slightly different JS than the user reporting the issue as I felt the CSRF input should be set upon "freeform-ready" event rather than the "freeform-ajax-before-submit" event fired on submit button clicks. When Stripe payment fields are included in the form, setting the CSRF input value on submit button click will not work as expected. Stripe grabs and uses the CSRF input upon page load when sending payment intent requests, which also happen on page load, not on submit button click.

<script>
    document.addEventListener("DOMContentLoaded", function () {
        const forms = document.querySelectorAll('[data-id="{{ form.anchor }}"]');
        forms.forEach(form => form.addEventListener("freeform-ready", function(event) {
            const freeform = event.target.freeform;

            event.options.scrollElement = freeform.form;

            fetch('/actions/users/session-info', { headers: { 'Accept': 'application/json' }})
                .then(response => response.json())
                .then(response => {
                    const csrfInput = freeform.form.querySelector('input[name=' + response.csrfTokenName + ']');
                    if (csrfInput) {
                        csrfInput.value = response.csrfTokenValue;
                    }
                });

            /*
            // https://docs.solspace.com/craft/freeform/v5/templates/caching/#static-caching--blitz
            fetch('/dynamic')
                .then(response => response.json())
                .then(response => {
                    if (response.csrf && response.csrf.name && response.csrf.value) {
                        const csrfInput = freeform.form.querySelector('input[name=' + response.csrf.name + ']');
                        if (csrfInput) {
                            csrfInput.value = response.csrf.value;
                        }
                    }
                });
            */
        }));
    });
    </script>

image

image

seandelaney avatar May 14 '25 11:05 seandelaney

I spent some time and found out that Craft supports a CSRF token passed as a header, instead of being an input field. (Though Yii supports that out of the box, it turns out Craft uses their own header for this)

I implemented an auto-fetch mechanism for CSRF tokens for ajax requests sent from throughout Freeform, which are then added as X-Craft-Csrf headers to the requests. The token is only fetched once per session (which might or might not be optimal, we have to see. But it works like it worked before, no refresh before submitting)

The current approach works in all scenarios and for cached templates out of the box, so there is no more need to fetch and replace csrf tokens by yourself for cached pages.

gustavs-gutmanis avatar May 15 '25 08:05 gustavs-gutmanis

@kjmartens

Tested G's changes without our manual CSRF JS approach with Multiple page form, Stripe and Blitz setup. All worked perfectly.

seandelaney avatar May 16 '25 10:05 seandelaney