nozzlegear / ShopifySharp

ShopifySharp is a .NET library that helps developers easily authenticate with and manage Shopify stores.
https://nozzlegear.com/shopify-development-handbook
MIT License
764 stars 313 forks source link

Cookie based authentication not working in safari #650

Closed zeshan-xeesoft closed 2 years ago

zeshan-xeesoft commented 3 years ago

We have developed shopify public app which uses cookie based authentication as suggested in The-Shopify-Development-Handbook.3.0.0 this works perfect with Chrome, Firefox and Edge but application failed to open on Safari browsers (version 13.x.x).

This issue occurs as Safari does not allow samesite=none for iframe's. Shopify suggest to use JWT tokens instead of cookies by using their app-bridge.

@nozzlegear can you guide us what changes we need to make in apps to make it compatible with Safari as in near future all other browsers will follow same security measures, which will effect apps badly.

nozzlegear commented 3 years ago

Hey @zeshan-xeesoft! You'll need to make a few changes to your application that will inspect the user agent and decide whether the browser supports 3rd-party cookies based off of that. I use something like this in my Startup.cs file:

using System;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AuntieDot.Data;
using AuntieDot.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;

namespace AuntieDot
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        private static void ConfigureCookieAuthentication(CookieAuthenticationOptions options)
        {
            options.Cookie.HttpOnly = true;
            options.SlidingExpiration = true;
            options.ExpireTimeSpan = TimeSpan.FromDays(1);
            options.LogoutPath = "/Auth/Logout";
            options.LoginPath = "/Auth/Login";
            options.AccessDeniedPath = "/Auth/Login";
            options.Cookie.SameSite = SameSiteMode.None;

            options.Validate();
        }

        /// <summary>
        /// Checks if the user agent does not support SameSiteMode.None.
        /// </summary>
        /// <remarks>
        /// Source: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
        /// </remarks>
        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;
        }

        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;
                }
            }
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {  
            // Cookies must be configured to use SameSite=None        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(ConfigureCookieAuthentication);
            // Configure Shopify-specific cookie and header options for loading the app in an embedded admin page
            services.AddAntiforgery(c =>
            {
                // All embedded apps are loaded in an iframe. The server must not send the X-Frame-Options: Deny header
                c.SuppressXFrameOptionsHeader = true;
                c.Cookie.SameSite = SameSiteMode.None;
            });
            // Configure cookie policy to check if the browser allows samesite
            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);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ...
        }
    }
}

Note that this won't let your JS scripts read the cookies from the browser. For that, you'll need to request access to Safari's Storage API too.

I've already sent out an update to the Handbook with .NET Core sample projects that have been configured to deal with Safari's changes! If you didn't get that update, just let me know by sending me an email at joshua@nozzlegear.com and I'll get it to you. I'm also planning to write a short new guide on dealing with these cookie issues with an ASP.NET Core backend + React frontend.

Anyway, let me know if this works for you!

zeshan-xeesoft commented 3 years ago

@nozzlegear thank you for your reply, here I want to mention that we are using asp.net mvc (.net framework 4.8) and on front-end we are using bootstrap with VanillaJS. We already have implemented changes described in this article which enabled our asp.net app for all browsers but recently Safari have implemented few changes for user privacy due to which iframe cannot access cookies inside Safari browsers.

I totally agree that we can use Safari Storage API which is recommended on another blog but it need user click to confirm storage access first, every time we load shopify app.

Instead of cookies, shopify recommends to use JWT tokens and app bridge. In following blog post they show How to fix slow app loading times and third-party cookie errors. In another web session they gives details about using JWT.

It seems we should make two necessary updates in our apps, one to use app bridge and second to use jwt tokens, this way we will have no cookie issues and on top our apps will be must faster. I have been trying to merge app-bridge with your apps but have not get any success yet.

I think we need to update our book and use app-bridge and JWT tokens, so our apps can work on all browsers without any issue.

Regarding Asp.net Core Updates, I have not received any email from your side. I will send you email from my registered email address so you can share latest copy of book. Thank you

zeshan-xeesoft commented 3 years ago

@nozzlegear are we planning authentication in our apps, based on JWT instead of Cookies ?