-
Notifications
You must be signed in to change notification settings - Fork 683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Top level partitioning for safari 12 #617
Conversation
if sanitized_shop_name.present? | ||
session['shopify.omniauth_params'] = { shop: sanitized_shop_name } | ||
fullpage_redirect_to "#{main_app.root_path}auth/shopify" | ||
if session['shopify.cookies_persist'] || !ShopifyApp.configuration.embedded_app? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a subtle bug here.
When we're in fullpage mode, we might already have the shopify.cookies_persist
cookie, which would mean we'd take this path. But the whole reason we're in fullpage mode is that we want to trigger a "fullpage redirect", since that's where our ITP logic currently lives.
We should separate these two concerns by fullpage redirecting to a set_top_level_cookie
action or passing a similarly-named parameter that will always trigger the top-level cookie (ITP) logic.
var versionNumber = isSafari ? parseFloat(userAgent.match(/Version\/(\d+\.?\d*)/)[1]) : null; | ||
|
||
// TODO: Replace with library for checking user-agents | ||
return document.hasStorageAccess && isSafari && versionNumber >= 12; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any way to do this as a feature check instead of using user agents? i'm concerned about the resiliency of this if, e.g., chrome or another browser go down the same path as safari. or (much less likely) if apple reverses course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about on Mobile?
POS: com.jadedpixel.pos Shopify POS/4.8.3 (iPhone; iOS 11.4; Scale/3.00) MobileMiddlewareSupported
Shopify Mobile:Shopify Mobile/iOS/7.6.0 (iPhone10,5 Simulator/com.shopify.ShopifyInternal/11.4.0)
Safari: Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
Do you have a way to test the behaviour on an iOS 12 device? Ive thus far been unable to see auth broken on my device running iOS 12 b6.
cc. @andrewapperley
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidmuzi I've also not seen apps broken. I'm running the latest iOS 12 beta as well. If we're using UserAgent checks then we would need to check for iOS 12
as well. I'll reply here if I see any announcement from Apple about ITP 2.0 on iOS or if its in the release notes of a new beta.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nwtn We were only doing feature detection before for document.hasStorageAccess
, and the ask to do feature detection came up in reviews with other stakeholders.
@andrewapperley Thanks for clarifying! I'll keep an eye out for announcements as well :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that a check for hasStorageAccess
should be sufficient, otherwise this may miss other webkit based browsers that implement hasStorageAccess
. Since storage access is baked right into webkit I expect other browsers will be affected over time. The list of webkit-based browsers is long, including Chrome on iOS according to this: https://en.wikipedia.org/wiki/List_of_web_browsers#WebKit-based
b77ae63
to
440d389
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really happy to see this being worked on! Thank you guys! :)
I've added some comments, mostly details. I haven't tested these changes.
@@ -57,14 +57,14 @@ def clear_shop_session | |||
session[:shopify_user] = nil | |||
end | |||
|
|||
def login_url | |||
def login_url(no_cookie_redirect = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better if no_cookie_redirect
was a keyword argument.
Right now the API to call this method is: https://github.com/Shopify/shopify_app/pull/617/files#diff-350e7187d4b8bbcd1eaee3ff44fc7fb3R68
login_url(true)
while, with keyword argument, it would change to:
login_url(no_cookie_redirect: true)
@@ -5,19 +5,19 @@ module LoginProtection | |||
class ShopifyDomainNotFound < StandardError; end | |||
|
|||
included do | |||
after_action :set_test_cookie if ShopifyApp.configuration.embedded_app? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any tests added for these changes (just some slight modifications). Can you please add some?
config/locales/ja.yml
Outdated
enable_cookies_heading: "Enable cookies from %{app}" | ||
enable_cookies_body: "You must manually enable cookies in this browser in order to use %{app} within Shopify." | ||
enable_cookies_footer: 'Cookies let the app authenticate you by temporarily storing your preferences and personal information. They expire after 30 days.' | ||
enable_cookies_action: 'Enable cookies' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These all are in English (I guess you know that :) ), and you're missing a new line here.
document.cookie = "shopify.cookies_persist=true"; | ||
window.location.href = window.shopOrigin + "/admin/apps/" + window.apiKey; | ||
|
||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need this. The button you've attached the "click" event at, it's type="button"
, therefore won't do any for submission or navigation. return false
to prevent clicks is generally considered to be a bad practice (due to stopping event propagation)
@@ -14,6 +14,10 @@ def create | |||
authenticate | |||
end | |||
|
|||
def enable_cookies | |||
@shop = sanitize_shop_param(params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could just reuse (call) sanitized_shop_name
from LoginProtection
. It does almost exactly and I like to avoid using @shop
variable name as a name for shop's domain. (Perhaps the best solution would be to rename the variable to @shop_domain
)
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<link rel="stylesheet" href="https://sdks.shopifycdn.com/polaris/2.5.0/polaris.min.css" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "redirect" page is loading 134KB of styles. Can you please extract the required styles and inline them like you did right here below?
} | ||
</style> | ||
<base target="_top"> | ||
<title>Redirecting…</title> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: You could use ...
(three characters) instead of …
(one character)
else | ||
redirect_to_login | ||
return redirect_to_login unless shop_session | ||
clear_top_level_oauth_cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary to call on every request?
Maybe it's better to simply ignore the cookie after you no longer need it? Or clear it somewhere during the authentication/cookie-setting process, once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't ignore it; we need to remove it or there's a risk that we won't run the full flow the next time we can't recover the session.
We need to wait until we confirm that we've actually recovered the session to remove the cookie, and this seems like the least bad place to do that. We could check that the cookie doesn't exist, but that's probably slower than just deleting the key from the hash regardless.
@@ -2,3 +2,7 @@ en: | |||
logged_out: 'Successfully logged out' | |||
could_not_log_in: 'Could not log in to Shopify store' | |||
invalid_shop_url: 'Invalid shop domain' | |||
enable_cookies_heading: "Enable cookies from %{app}" | |||
enable_cookies_body: "You must manually enable cookies in this browser in order to use %{app} within Shopify." | |||
enable_cookies_footer: 'Cookies let the app authenticate you by temporarily storing your preferences and personal information. They expire after 30 days.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do cookies really expire after 30 days? I think this depends on the app, and, as far as I can tell, rails doesn't set expiration on cookies. Sorry I couldn't find anything more useful. :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @vfonic, this is enforced by ITP: https://webkit.org/blog/8311/intelligent-tracking-prevention-2-0/, and is the reason this branch is being worked on.
d4d152c
to
1534963
Compare
One thing I think we should do: we should check the user agent for POS/mobile and skip the "enable cookies" button in that case. We know that ITP doesn't apply because we're not using SafariViewControllers to render the embedded apps, but there's no easy way for the app to know that. I know we were opposed to this for the purposes of determining whether ITP applied in general, but if the user agents are stable enough I think it makes sense in this context. |
39412d9
to
83eed27
Compare
cc @ShayneP |
58e7c56
to
5e920ff
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few suggestions, but looks good!
@@ -0,0 +1,16 @@ | |||
function setCookieAndRedirect() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it may be wise to wrap all this code in an IIFE just for safety and to avoid interference from browser extensions, etc.
@@ -1,5 +1,10 @@ | |||
document.addEventListener("DOMContentLoaded", function() { | |||
function redirect() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same suggestion as above
<base target="_top"> | ||
<title>Redirecting…</title> | ||
|
||
<script src="https://cdn.shopify.com/s/assets/external/app.js?<%= Time.now.strftime('%Y%m%d%H') %>"></script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to load the EASDK here since we're not initializing it?
|
||
document.addEventListener("DOMContentLoaded", function() { | ||
if (document.hasStorageAccess) { | ||
var itpContent = document.querySelector('#CookiePartitionPrompt'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor optimization, but getElementById
is faster than querySelector
with an id (ref)
@@ -0,0 +1,388 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presumably this should change based on locale
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels super fragile. I’m assuming you’re embedding this here for performance, so we’re not loading all the Polaris CSS for a single redirect page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that was the logic. I know we got some pushback around including all of the CSS given the size, especially for something that's relatively static like this page.
0e02279
to
5deb12f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but given there has been a lot of changes and conditions added to the session controller, I would suggest more 👀and 👍 before you merge the changes
ITP doesn't apply to WebKitView
a4c808e
to
27c76e3
Compare
Webkit is releasing ITP 2.0 as part of the OS updates scheduled for Fall 2018. These updates, which will affect merchants using Safari 12, break embedded apps in Shopify admin by preventing apps from using cookies in the iframe. Apps rely on these cookies to authenticate the merchant when the app loads in an iframe.
This PR works around ITP 2.0 by triggering "cookie partitioning". Partitioning is triggered when the user interacts with a domain at the top-level before interacting with it in the iframe. In the partitioned state apps are able to use cookies in the iframe, but those cookies aren't available outside of the iframe context.
These limitations required us to implement a more convoluted flow to ensure that we auth correctly. That flow is diagrammed below, and this PR implements that flow: