Shopify / shopify_app

A Rails Engine for building Shopify Apps
MIT License
1.76k stars 687 forks source link

Blocking 3rd party cookies breaks most apps #769

Closed fancydev18 closed 1 year ago

fancydev18 commented 5 years ago

Blocking 3rd party cookies breaks most apps. This will be the default behavior on Chrome too.

There was already some work done to alleviate this: https://github.com/Shopify/shopify_app/issues/711 but that's hardly enough.

Since Shopify forces app developers to run apps in embedded SDK iframe, instead of own app domain, we believe Shopify should do more to help with making this work. Apps trying workarounds are even rejected: https://community.shopify.com/c/Shopify-APIs-SDKs/How-to-enable-third-party-cookies-embedded-apps/td-p/439425

Proposals:

  1. When in Apps screen, before even clicking on an app, check if 3rd party cookies are blocked. If so, show a warning message at top that Apps require 3rd party cookies to be enabled, and link to a doc on how to enable them, eg https://support.boldcommerce.com/hc/en-us/articles/202650579-Enable-Third-Party-Cookies

  2. Chrome prevents server side scripts to read httponly cookies when the request is initiated via 302 redirect. Shopify uses a 302 redirect to "App URL" when loading the app. As a workaround, when 3rd party cookies are blocked, show again the warning and an "Open app" link for merchants to click, instead of a redirect.

Note that this is hard to handle by app developers from client side, as we don't even get to execute our app JS, since the auth is stuck between "App URL" and the "Whitelisted redirection URL(s)" (which does work and can read cookies properly set by "App URL", since the redirect is on the same host).

Thanks

nwtn commented 5 years ago

Thanks for taking the time to document this @fancydev18. You have some great points in here! At a high-level, know that this is something we’re aware of and working to improve (both in a short-term, stop-the-bleeding capacity, and in a longer-term manner).

Blocking 3rd party cookies breaks most apps. This will be the default behavior on Chrome too.

We’re aware of this and are looking into using the SameSite attribute to help with this.

Since Shopify forces app developers to run apps in embedded SDK iframe, instead of own app domain, we believe Shopify should do more to help with making this work. Apps trying workarounds are even rejected: community.shopify.com/c/Shopify-APIs-SDKs/How-to-enable-third-party-cookies-embedded-apps/td-p/439425

Shopify doesn’t force app developers to use embedded apps; many successful apps on our app store are not embedded, and for the time-being we’re not considering making embedded apps a requirement.

That said we do recommend developers embed their apps in Shopify, because in general this creates a better experience for the merchant. When embedding an app, there are certain best practices we recommend, and others we enforce. We currently have some documentation about authenticating embedded apps with OAuth that covers how to make sure the Oauth redirects work properly. One thing not covered there (which we should add; I’ll open a ticket internally about this) is the UX of this experience — if the app renders UI during this redirect step, it creates visible flashes of content that can be confusing for the merchant. Instead, we recommend rendering a plain blank/white page during the Oauth redirect steps. In addition to making the redirects less jarring, this has the added benefit of speeding the Oauth process up (since the browser doesn’t have to waste time downloading/rendering content).

Proposals:

These are great suggestions! I’ll keep this issue open until we can implement some of these or come up with equivalent/better solutions.

fancydev18 commented 5 years ago

Thanks for considering, Dave!

We’re aware of this and are looking into using the SameSite attribute to help with this.

I already tried setting the session cookie with SameSite: Lax, but it didn't have any effect (cookies still weren't sent after 302 redirects). This makes sense, as otherwise all trackers would set their cookies to Lax and keep working, instead of being blocked. Would this be something that would be set by the top domain which embeds the app, on your side? How would it work and when could we test this?

Looking forward to any improvements/best-practices.

ragalie commented 5 years ago

I already tried setting the session cookie with SameSite: Lax

I think you'll want to use SameSite: None, based on the description here: https://web.dev/samesite-cookies-explained. Can you give it a try and see if that works for you?

fancydev18 commented 5 years ago

Thanks, Mike. With 'Block third-party cookies and site data' enabled in Chrome, it still doesn't work. Which makes sense, if users chose to Block third-party cookies, they won't be sent no matter what 3rd party sites request, including the SameSite header. Otherwise, the setting would be useless.

Is there anything Shopify embedded app developers could do to make apps work with 'Block third-party cookies and site data' enabled? The alternative that we considered was not using cookies after the app loads, but pass a token from server which the app frontend will send on every AJAX request. However, the app doesn't get to being loaded, since the cookie that we check for auth is blocked.

fancydev18 commented 4 years ago

ping. Chrome is making third party cookies obsolete: https://blog.chromium.org/2020/01/building-more-private-web-path-towards.html

Thanks

sillycube commented 4 years ago

Some users report the issue after the recent Chrome update. I find it hard for users to follow our tutorial to unblock 3rd party cookies. And I don't think embedded apps create a good experience for the merchants if they're going to unblock the cookie for each app.

tanema commented 4 years ago

Please update shopify_app if you have issues. We have added SameSite cookie policies to cookies set in embedded apps.

fancydev18 commented 4 years ago

What is the solution for embedded apps to work with 3rd party cookies BLOCKED? SameSite is not a solution when 3rd party cookies are fully blocked.

And in the meantime, Shopify only blames app developers: Screenshot from 2020-04-10 12-31-03

https://community.shopify.com/c/Shopify-Apps/Some-Third-Party-Apps-may-not-be-accessible-on-Chrome-80-update/m-p/650220/thread-id/20503

sillycube commented 4 years ago

What is the solution for embedded apps to work with 3rd party cookies BLOCKED? SameSite is not a solution when 3rd party cookies are fully blocked.

And in the meantime, Shopify only blames app developers: Screenshot from 2020-04-10 12-31-03

https://community.shopify.com/c/Shopify-Apps/Some-Third-Party-Apps-may-not-be-accessible-on-Chrome-80-update/m-p/650220/thread-id/20503

My embedded app can't be opened in safari. I guess there are 2 ways to handle the issue now. Either removing all cookies or asking user to manually allow 3rd party cookies.

ahurlburt commented 4 years ago

Is this doc still accurate: https://shopify.dev/tutorials/migrate-your-app-to-support-samesite-cookies?

I have made the changes to set the samesite and secure attributes and thought my app was working fine after installation however after restarting browser it is stuck in an endless redirect loop throwing app bridge errors, when I check the chrome issues I see the cookie "shopifyTopLevelOAuth" for my domain is being blocked.

I can get the app to work again if I manually call /auth?shop=XXX, but then after restarting browser I get stuck in the same loop.

I'm not sure where that shopify cookie is being set but something is not working correctly. My auth follows the example for node/react and was updated to support same site cookies according to the doc I mentioned at the start of this post.

ahurlburt commented 4 years ago

Some additional information:

After selecting the app from shopify admin this is the response I get back:

HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Thu, 13 Aug 2020 13:02:39 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Set-Cookie: shopifyTopLevelOAuth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; httponly X-Powered-By: Next.js ETag: "28c5-e7iyK0tvAa0ccy4qire7+lwH0yw" Vary: Accept-Encoding Content-Encoding: gzip

There is a warning in chrome indicating the cookie for shopifyTopLevelOAuth was blocked because it is missing the samesite attribute. From what I can tell koa-shopify-auth package should be setting this response. I am using the latest package: 

"@shopify/koa-shopify-auth": "^3.1.65",

On further inspection I am wondering if this line in koa-shopify-auth is the problem since it does not set the samesite attribute for the shopifyTopLevelOAuth cookie?

https://github.com/Shopify/quilt/blob/5ba6cd69793a071950467c5f09c4dee9e93d15d0/packages/koa-shopify-auth/src/verify-request/verify-token.ts#L18

everywhere else in koa-shopify-auth where the top level cookie is set, the getCookieOptions(ctx) params are passed

tanema commented 4 years ago

@ahurlburt Are you writing a node app? If so you can inquire for more information in https://github.com/shopify/quilt

anuragraina commented 4 years ago

I am using express and had the same issue recently, finally resolved after spending quite some time on it.

Some solutions are available for koa framework, but none for express:

So here's how I resolved it:

Checkout this link for koa framework : https://community.shopify.com/c/Shopify-APIs-SDKs/Samesite-Cookie-Enforcement-causing-internal-server-error/m-p/641846#M4

With reference to this, your express code should be as follows:

  1. Set additional option when setting express-session: const session = require('express-session'); app.use( session({........ , cookie : { secure : true, sameSite : 'none' } }) );

  2. When sending Shop cookie pass the same options above: res.cookie(name, value, { secure : true, sameSite : 'none' });

  3. However, above will generate an error: Error: Cannot send secure cookie over unencrypted connection To avoid the error, set: app.set('trust proxy', true);

Hopefully this should do the trick.

mariusa commented 4 years ago

Could you set your Chrome to block all 3rd party cookies, then test? I don't see how this can work, no matter what you do on server. It's a 3rd party cookie.

ragalie commented 4 years ago

You may want to check out the new App Bridge auth that doesn't rely on cookies: https://shopify.dev/tools/app-bridge/authentication

This should be more reliable, performant and easier to maintain over the long-term.

adrianocr commented 4 years ago

@ragalie can I trouble you for some info on the flow of how that's supposed to work?

Do I have these steps correctly? Assuming the app has NOT been installed on your store yet:

  1. Shopify makes request to your app URL, you serve the frontend unauthenticated
  2. frontend initializes app bridge
  3. frontend redirects to install/permissions URL
  4. user grants permissions
  5. shopify redirects to your redirect uri (in my case, /login/callback)
  6. the /login/callback route redirects back to your application at https://{shop}.myshopify.com/admin/apps/{API-KEY-HERE}
  7. shopify loads your frontend again, this time you don't redirect and use getSessionToken() to get the JWT

If the above flow is correct, the part I'm having a hard time with is step 3, 6, and 7.

For step #1 and #7, how do you check to see if the app has already been installed and doesn't need to be redirected to the install/permissions URL? Normally with the backend based approach you'd check for a session on the browser <-> server. Should I set a cookie in step #6 and check for it on step #1? And then for step #6, on the following link: https://shopify.dev/tools/app-bridge/getting-started, the docs says "Do not construct a Shopify admin URL manually". But this callback URL loads inside of a new window/tab that you can't initialize app bridge in (since it isn't inside an iframe) and therefore can't use the redirect action from appbridge to redirect to the admin backend. So is the docs incorrect or am I missing something?

ragalie commented 4 years ago

I think your step 1 isn't what we recommend.

In step 1, a user will be visiting your app and there will be a shop param. You should check on your backend whether you already have an Admin API access token for that shop. If you do, then render the frontend (and step 7 occurs). If you don't, then your backend redirects to https://{shop}.myshopify.com/admin/oauth/authorize?..., and the rest of the steps occur as you wrote them out.

You can see the module that implements this logic for the shopify_app gem here: https://github.com/Shopify/shopify_app/blob/master/app/controllers/concerns/shopify_app/require_known_shop.rb

These callbacks run on the unauthenticated controller.

Shine18 commented 4 years ago

Hi, any solution to this. I developed an app and it has the same issue. I set the cookie same site attribute with secure_headers gem. I also used the rails_same_site_cookie gem. But nothing works.

fancydev18 commented 3 years ago

Hi @ragalie

In step 1, a user will be visiting your app and there will be a shop param. You should check on your backend whether you already have an Admin API access token for that shop. If you do, then render the frontend (and step 7 occurs). If you don't, then your backend redirects to https://{shop}.myshopify.com/admin/oauth/authorize?..., and the rest of the steps occur as you wrote them out.

Since we can't use sessions anymore (no 3rd party cookies) , how should the frontend know the shop origin? https://shopify.dev/tutorials/get-and-store-the-shop-origin

That is required to initialize Shopify bridge, which only afterwards can be used to get the JWT token.

paulomarg commented 3 years ago

Hi @fancydev18, Shopify Admin will include the shop in the URL when it loads your app, so you'll be able to get the domain from there.

github-actions[bot] commented 1 year ago

This issue is stale because it has been open for 90 days with no activity. It will be closed if no further action occurs in 14 days.

github-actions[bot] commented 1 year ago

We are closing this issue because it has been inactive for a few months. This probably means that it is not reproducible or it has been fixed in a newer version. If it’s an enhancement and hasn’t been taken on since it was submitted, then it seems other issues have taken priority.

If you still encounter this issue with the latest stable version, please reopen using the issue template. You can also contribute directly by submitting a pull request– see the CONTRIBUTING.md file for guidelines

Thank you!