How to get an Asp.Net Core Cookies and AntiForgery Tokens to work in an embedded Shopify app

Building embedded Shopify apps for the Shopify app store is a great way to integrate your app or service more tightly with merchants' Shopify stores. Embedded apps are loaded directly inside each Shopify store's admin dashboard, increasing the likelihood that your app will be used since merchants don't need to navigate away to a completely different URL. However, if you're using C# and Asp.Net, you'll likely run into issues like the following:

  • HTTP requests don't send cookies from the browser to the server, making the user appear to be logged out.
  • Any page with AntiForgery tokens fails to load, showing a browser error instead.
  • The framework refuses to validate AntiForgery tokens and returns a 415 Method Not Allowed response.

AntiForgery can cause browsers to refuse to load pages in a Shopify embedded app

The source of these woes comes down to the fact that embedded Shopify apps are loaded inside HTML iframes within the Shopify admin dashboard. But because it's loaded in an iframe, cookies and the AntiForgery tokens that rely on them don't work the way they usually would. That's because, after updates to the HTTP Cookie design spec in 2020, cookies will not be sent in most HTTP requests when loaded from a cross-site origin -- i.e. an iframe.

I wrote a book on building rock-solid Shopify apps with C# and Asp.Net -- it comes with four sample projects, and covers building embedded Shopify apps, using proxy pages, webhooks, and more! Purchase it on Gumroad.

However this is a relatively new problem, as up until recently cookies would be sent through cross-site requests. It all changed when the default value for the SameSite cookie attribute was changed by Google Chrome -- introducing new default behavior that prevents these cookies from going through cross-site requests. In practical terms, this means our cookies don't work in embedded Shopify apps, and that Asp.Net cannot validate AntiForgery tokens because they rely partly on cookies.

An example where the app's cookies are blocked when loaded in Shopify's dashboard

According to MDN, HTTP cookies can have three different SameSite attribute values:

  1. Lax: When this mode is used, cookies will not be sent on cross-site requests, e.g. when loading images or iframes, but they are sent when the user clicks on a link to the site. This is the default mode for all cookies when a SameSite mode is not specified.
  2. Strict: Cookies will only be sent in first-party contexts. They will never be sent when loading images or iframes, or when clicking on a link to the site.
  3. None: Cookies will be sent in all contexts, including requests made from iframes. If this mode is used, the cookie must also set Secure=true ensuring the cookie will only ever be sent over secure (https) connections. This used to be (and still is) the default value for some browsers before it was changed to Lax in modern versions.

Internet Explorer 11 does not support the SameSite attribute and ignores it entirely, effectively defaulting the SameSite mode to None. Chrome, on the other hand, updated their implementation in 2020 and switched the default value from None to Lax, and modern browsers were forced to follow this change. This means that older versions of Chrome treat cross-site cookies differently than newer versions of Chrome.

Luckily the fix for this can all be done with a little bit of extra configuration code in your Asp.Net app's Startup.cs file. We'll reset the framework's default value for a cookie's SameSite mode back to the original None (from Lax), allowing cookies to once again be sent across requests when the app is loaded in an iframe.

We also need to do just a little bit of user-agent sniffing when setting cookies because certain versions of certain browsers do not support None for a SameSite mode:

  • Safari on iOS 12
  • Webview on iOS 12
  • Chrome on iOS 12
  • Firefox on iOS 12
  • Safari on macOS 14
  • Google Chrome versions 50-69

So if any of these browsers are being used, the app will need to override and unset the SameSite mode entirely by using the SameSiteMode.Unspecified enum. This will effectively achieve the same result as setting the mode to None, allowing the cookies to be sent through cross-site requests in those browsers. And if they're using any other browser than the ones listed above, we know we can use SameSite mode None without breaking anything or altering our expected behavior.

To get started, open up your app's Startup.cs file. First up, you'll need to add a new function to configure your cookie authentication options (assuming you're using cookies for authentication). You might already have this function, and you can customize any part of it you like -- the only important part is setting options.Cookie.SameSite value to SameSiteMode.None:

// In Startup.cs

private static void ConfigureCookieAuthentication(CookieAuthenticationOptions options)
{
    // Set the default SameSite mode to None
    options.Cookie.SameSite = SameSiteMode.None
    // Customize the rest of the cookie options here if necessary
    //options.Cookie...

    options.Validate()
}

Following that, we add a function that tells us if the user's browser does not allow None as a valid SameSite mode. We do this by checking the user agent:

Disclaimer: this code comes from Microsoft's blog post on the SameSite cookie changes.

// In Startup.cs

private bool DisallowsSameSiteNone(string userAgent)
{
    if (string.IsNullOrEmpty(userAgent))
    {
        return false;
    }

    // Cover all iOS based browsers here. This includes:
    // - Safari on iOS 12 for iPhone, iPod Touch, iPad
    // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
    // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
    // All of which are broken by SameSite=None, because they use the iOS networking stack
    if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12"))
    {
        return true;
    }
 
    // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes:
    // - Safari on Mac OS X.
    // This does not include:
    // - Chrome on Mac OS X
    // Because they do not use the Mac OS networking stack.
    if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
        userAgent.Contains("Version/") && userAgent.Contains("Safari"))
    {
        return true;
    }
 
    // Cover Chrome 50-69, because some versions are broken by SameSite=None,
    // and none in this range require it.
    // Note: this covers some pre-Chromium Edge versions,
    // but pre-Chromium Edge does not require SameSite=None.
    if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
    {
        return true;
    }
 
    return false;
}

And then we need to add a smaller function to pull out the user-agent string from the request headers and pass it to that function we just wrote. If the user-agent string indicates that we can't use None for our SameSite mode, we'll completely unset the SameSite attribute so it won't be sent at all. This will achieve the same result in those browsers that do not allow None:

// In Startup.cs

// ...

private void CheckSameSite(HttpContext context, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = context.Request.Headers["User-Agent"].ToString();

        if (DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
        }
    }
}

And with those three functions written, we just need to configure the Asp.Net cookie service to use them. You should have a method in your Startup class called ConfigureServices -- find that method, and inside it we're going to add several lines of code which will configure the framework's cookie policy to do the following:

  1. Set the minimum SameSite mode to Unspecified, which will prevent the SameSite mode from being added to cookies unless explicitly set.
  2. Set the cookies to always use Secure mode, which is required when using SameSite mode None.
  3. Call our new CheckSameSite function when appending or deleting cookies, so that we can verify the user's browser and decide if using None is supported.
// In Startup.cs

// ...

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
        options.Secure = CookieSecurePolicy.Always;
        options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
}

And with those few changes, your cookies should start sending across the iframe requests when your embedded app is loaded in the Shopify admin dashboard. If you're using AntiForgery tokens, there's one more change you need to make to get them working as well. The AntiForgery service uses its own cookie policy, so we need to configure the service to use the right SameSite mode. The service will also put out an X-Frame-Options: Deny header, which we'll need to turn off; it prevents the app from loading at all in an iframe on pages that use the tokens.

Find the line of code in your Startup class that adds the AntiForgery service and add the following configuration code:

// In Startup.cs

// ...

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddAntiforgery(options => 
    {
        options.SuppressXFrameOptionsHeader = true;
        options.Cookie.SameSite = SameSiteMode.None;
    });
}

There you have it! With these few changes, your embedded Asp.Net app should once again start working inside Shopify's admin dashboard.

The embedded Shopify app built with Asp.Net Core loads in an iframe after making the changes above


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.