Rafael Corrêa Gomes

How to set up CSRF protection on Shopify Apps?

To make a POST request with CSRF protection while using the shopify.auth middleware, you need to do the following:

In web/config/session.php

return [
    'secure' => true,
    'same_site' => 'none'
];

Add a route in your routes/web.php file to generate a CSRF token:

web/routes/web.php

Route::get('/api/csrf-token', fn () => ['csrf_token' => csrf_token()]);

Create a hook for fetching the CSRF token:

web/frontend/hooks/useCsrf.js

import { useAuthenticatedFetch } from './useAuthenticatedFetch'

/**
 * A hook for fetching a CSRF token from the server.
 *
 * The CSRF token MUST be included in every POST, PUT, PATCH, or DELETE
 * request sent to the server as a property called "_token" in the JSON body
 * or by header as X-CSRF-TOKEN.
 *
 * @returns {Function}
 */
export const useCsrf = () => {
  const fetch = useAuthenticatedFetch()

  return async () => {
    const response = await fetch('/api/csrf-token')
    const { csrf_token } = await response.json()

    return csrf_token
  }
}

Then, when submitting a non-GET request, fetch it before submitting the form, for example:

import { Form } from '@shopify/polaris'
import { useAuthenticatedFetch } from '../../hooks/useAuthenticatedFetch'
import { useCsrf } from '../../hooks/useCsrf'

export default function MyComponent() {
  const fetch = useAuthenticatedFetch()
  const csrf = useCsrf()

  const handleSubmit = async formValues => {
    const csrfToken = await csrf()

    const response = await fetch('/api/somewhere', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': csrfToken  // Include as header
      },
      body: JSON.stringify({
        _token: csrfToken    // OR include as "_token" property on the body, either works
        ...formValues
      })
    })
  }

  return (
    <Form onSubmit={handleSubmit}>
      {/* form contents */}
    </Form>
  )
}

With the hook, you can now easily include CSRF protection on all forms in your React frontend.

Because the React frontend and Laravel backend have different origins, the default ‘same_site’ => ‘lax’ in config/session.php prevents cookies from being correctly in the browser. Setting this to ‘none’ allows the cookies to be set. Settings ‘secure’ to true also ensures the cookies are only set over HTTPS. Once you’ve changed those values, you should be able to see your session cookie stored in the browser. Without the session cookie, the Laravel backend will regenerate a CSRF token on every request, and you will always end up getting a CSRF token mismatch error.

TIP: Always include the 'Content-Type': 'application/json' header when making a POST request, or your Laravel’s request body will be empty.

About me

Picture of Rafael Corrêa Gomes

Rafael Corrêa Gomes

Senior e-commerce developer and architect based in Montreal, Canada. More than ten years of experience developing e-commerces, saas products and managing teams working with Magento, Shopify, PHP, JavaScript, and NodeJS.