${HEADING}
+${BODY}
+diff --git a/packages/koa-shopify-auth/CHANGELOG.md b/packages/koa-shopify-auth/CHANGELOG.md index 8ae15d4b4b..04ad07cef1 100644 --- a/packages/koa-shopify-auth/CHANGELOG.md +++ b/packages/koa-shopify-auth/CHANGELOG.md @@ -7,6 +7,10 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [3.1.61] - 2020-05-01 + +- Fixes ITP 2.3 and Safari 13.1 enable cookies loop [1413](https://github.com/Shopify/quilt/pull/1413) + ## [3.1.56] - 2020-02-03 - Package now forces cookies.secure to be true [1255](https://github.com/Shopify/quilt/pull/1255) diff --git a/packages/koa-shopify-auth/src/auth/client/itp-helper.ts b/packages/koa-shopify-auth/src/auth/client/itp-helper.ts new file mode 100644 index 0000000000..9529820440 --- /dev/null +++ b/packages/koa-shopify-auth/src/auth/client/itp-helper.ts @@ -0,0 +1,43 @@ +// Copied from https://github.com/Shopify/shopify_app +const itpHelper = `(function() { + function ITPHelper(opts) { + this.itpContent = document.getElementById('TopLevelInteractionContent'); + this.itpAction = document.getElementById('TopLevelInteractionButton'); + this.redirectUrl = opts.redirectUrl; + } + + ITPHelper.prototype.redirect = function() { + sessionStorage.setItem('shopify.top_level_interaction', true); + window.location.href = this.redirectUrl; + } + + ITPHelper.prototype.userAgentIsAffected = function() { + return Boolean(document.hasStorageAccess); + } + + ITPHelper.prototype.canPartitionCookies = function() { + var versionRegEx = /Version\\/12\\.0\\.?\\d? Safari/; + return versionRegEx.test(navigator.userAgent); + } + + ITPHelper.prototype.setUpContent = function(onClick) { + this.itpContent.style.display = 'block'; + this.itpAction.addEventListener('click', this.redirect.bind(this)); + } + + ITPHelper.prototype.execute = function() { + if (!this.itpContent) { + return; + } + + if (this.userAgentIsAffected()) { + this.setUpContent(); + } else { + this.redirect(); + } + } + + this.ITPHelper = ITPHelper; + })(window);`; + +export default itpHelper; diff --git a/packages/koa-shopify-auth/src/auth/client/polaris-css.ts b/packages/koa-shopify-auth/src/auth/client/polaris-css.ts new file mode 100644 index 0000000000..446383a7fe --- /dev/null +++ b/packages/koa-shopify-auth/src/auth/client/polaris-css.ts @@ -0,0 +1,341 @@ +const polarisCss = `html, +body { + min-height: 100%; + height: 100%; + font-size: 1.5rem; + font-weight: 400; + line-height: 2rem; + text-transform: initial; + letter-spacing: initial; + font-weight: 400; + color: #212b36; + font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto, + Segoe UI, Helvetica Neue, sans-serif; +} + +@media (min-width: 40em) { + html, + body { + font-size: 1.4rem; + } +} + +html { + position: relative; + font-size: 62.5%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; + text-rendering: optimizeLegibility; +} + +body { + min-height: 100%; + margin: 0; + padding: 0; + background-color: #f4f6f8; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p { + margin: 0; + font-size: 1em; + font-weight: 400; +} + +#CookiePartitionPrompt, #RequestStorageAccess { + display: none; +} + +.Polaris-Page { + margin: 0 auto; + padding: 0; + max-width: 99.8rem; +} + +@media (min-width: 30.625em) { + .Polaris-Page { + padding: 0 2rem; + } +} +@media (min-width: 46.5em) { + .Polaris-Page { + padding: 0 3.2rem; + } +} + +.Polaris-Page__Content { + margin: 2rem 0; +} + +@media (min-width: 46.5em) { + .Polaris-Page__Content { + margin-top: 2rem; + } +} + +@media (min-width: 46.5em) { + .Polaris-Page { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + } +} + +.Polaris-Layout { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + margin-top: -2rem; + margin-left: -2rem; +} + +.Polaris-Layout__Section { + -webkit-box-flex: 2; + -ms-flex: 2 2 48rem; + flex: 2 2 48rem; + min-width: 51%; +} + +.Polaris-Layout__Section--fullWidth { + -webkit-box-flex: 1; + -ms-flex: 1 1 100%; + flex: 1 1 100%; +} + +.Polaris-Layout__Section { + max-width: calc(100% - 2rem); + margin-top: 2rem; + margin-left: 2rem; +} + +.Polaris-Stack { + margin-top: -1.6rem; + margin-left: -1.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; +} + +.Polaris-Stack > .Polaris-Stack__Item { + margin-top: 1.6rem; + margin-left: 1.6rem; + max-width: calc(100% - 1.6rem); +} + +.Polaris-Stack__Item { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + min-width: 0; +} + +.Polaris-Heading { + font-size: 1.7rem; + font-weight: 600; + line-height: 2.4rem; + margin: 0; +} + +@media (min-width: 40em) { + .Polaris-Heading { + font-size: 1.6rem; + } +} + +.Polaris-Card { + overflow: hidden; + background-color: white; + box-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05), + 0 1px 3px 0 rgba(63, 63, 68, 0.15); +} + +.Polaris-Card + .Polaris-Card { + margin-top: 2rem; +} + +@media (min-width: 30.625em) { + .Polaris-Card { + border-radius: 3px; + } +} + +.Polaris-Card__Header { + padding: 2rem 2rem 0; +} + +.Polaris-Card__Section { + padding: 2rem; +} + +.Polaris-Card__Section + .Polaris-Card__Section { + border-top: 1px solid #dfe3e8; +} + +.Polaris-Card__Section--subdued { + background-color: #f9fafb; +} + +.Polaris-Stack--distributionTrailing { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.Polaris-Stack--vertical { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +.Polaris-Button { + fill: #637381; + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + min-height: 3.6rem; + min-width: 3.6rem; + margin: 0; + padding: 0.7rem 1.6rem; + background: linear-gradient(to bottom, white, #f9fafb); + border: 1px solid #c4cdd5; + box-shadow: 0 1px 0 0 rgba(22, 29, 37, 0.05); + border-radius: 3px; + line-height: 1; + color: #212b36; + text-align: center; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-decoration: none; + transition-property: background, border, box-shadow; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.64, 0, 0.35, 1); +} + +.Polaris-Button:hover { + background: linear-gradient(to bottom, #f9fafb, #f4f6f8); + border-color: #c4cdd5; +} + +.Polaris-Button:focus { + border-color: #5c6ac4; + outline: 0; + box-shadow: 0 0 0 1px #5c6ac4; +} + +.Polaris-Button:active { + background: linear-gradient(to bottom, #f4f6f8, #f4f6f8); + border-color: #c4cdd5; + box-shadow: 0 0 0 0 transparent, inset 0 1px 1px 0 rgba(99, 115, 129, 0.1), + inset 0 1px 4px 0 rgba(99, 115, 129, 0.2); +} + +.Polaris-Button.Polaris-Button--disabled { + fill: #919eab; + transition: none; + background: linear-gradient(to bottom, #f4f6f8, #f4f6f8); + color: #919eab; +} + +.Polaris-Button__Content { + font-size: 1.5rem; + font-weight: 400; + line-height: 1.6rem; + text-transform: initial; + letter-spacing: initial; + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-width: 1px; + min-height: 1px; +} + +@media (min-width: 40em) { + .Polaris-Button__Content { + font-size: 1.4rem; + } +} + +.Polaris-Button--primary { + background: linear-gradient(to bottom, #6371c7, #5563c1); + border-color: #3f4eae; + box-shadow: inset 0 1px 0 0 #6774c8, 0 1px 0 0 rgba(22, 29, 37, 0.05), + 0 0 0 0 transparent; + color: white; + fill: white; +} + +.Polaris-Button--primary:hover { + background: linear-gradient(to bottom, #5c6ac4, #4959bd); + border-color: #3f4eae; + color: white; + text-decoration: none; +} + +.Polaris-Button--primary:focus { + border-color: #202e78; + box-shadow: inset 0 1px 0 0 #6f7bcb, 0 1px 0 0 rgba(22, 29, 37, 0.05), + 0 0 0 1px #202e78; +} + +.Polaris-Button--primary:active { + background: linear-gradient(to bottom, #3f4eae, #3f4eae); + border-color: #38469b; + box-shadow: inset 0 0 0 0 transparent, 0 1px 0 0 rgba(22, 29, 37, 0.05), + 0 0 1px 0 #38469b; +} + +.Polaris-Button--primary.Polaris-Button--disabled { + fill: white; + background: linear-gradient(to bottom, #bac0e6, #bac0e6); + border-color: #a7aedf; + box-shadow: none; + color: white; +}`; + +export default polarisCss; diff --git a/packages/koa-shopify-auth/src/auth/client/request-storage-access.ts b/packages/koa-shopify-auth/src/auth/client/request-storage-access.ts new file mode 100644 index 0000000000..2e51b6eddf --- /dev/null +++ b/packages/koa-shopify-auth/src/auth/client/request-storage-access.ts @@ -0,0 +1,25 @@ +// Copied from https://github.com/Shopify/shopify_app +const requestStorageAccess = (shop: string) => { + return `(function() { + function redirect() { + var targetInfo = { + myshopifyUrl: "https://${shop}", + hasStorageAccessUrl: "/auth/inline?shop=${shop}", + doesNotHaveStorageAccessUrl: "/auth/enable_cookies?shop=${shop}", + appTargetUrl: "/?shop=${shop}" + } + + if (window.top == window.self) { + // If the current window is the 'parent', change the URL by setting location.href + window.top.location.href = targetInfo.hasStorageAccessUrl; + } else { + var storageAccessHelper = new StorageAccessHelper(targetInfo); + storageAccessHelper.execute(); + } + } + + document.addEventListener("DOMContentLoaded", redirect); + })();`; +}; + +export default requestStorageAccess; diff --git a/packages/koa-shopify-auth/src/auth/client/storage-access-helper.ts b/packages/koa-shopify-auth/src/auth/client/storage-access-helper.ts new file mode 100644 index 0000000000..96c30c32b4 --- /dev/null +++ b/packages/koa-shopify-auth/src/auth/client/storage-access-helper.ts @@ -0,0 +1,151 @@ +// Copied from https://github.com/Shopify/shopify_app +const storageAccessHelper = `(function() { + var ACCESS_GRANTED_STATUS = 'storage_access_granted'; + var ACCESS_DENIED_STATUS = 'storage_access_denied'; + + function StorageAccessHelper(redirectData) { + this.redirectData = redirectData; + } + + StorageAccessHelper.prototype.setNormalizedLink = function(storageAccessStatus) { + return storageAccessStatus === ACCESS_GRANTED_STATUS ? this.redirectData.hasStorageAccessUrl : this.redirectData.doesNotHaveStorageAccessUrl; + } + + StorageAccessHelper.prototype.redirectToAppTLD = function(storageAccessStatus) { + var normalizedLink = document.createElement('a'); + + normalizedLink.href = this.setNormalizedLink(storageAccessStatus); + + data = JSON.stringify({ + message: 'Shopify.API.remoteRedirect', + data: { + location: normalizedLink.href, + } + }); + window.parent.postMessage(data, this.redirectData.myshopifyUrl); + } + + StorageAccessHelper.prototype.redirectToAppsIndex = function() { + window.parent.location.href = this.redirectData.myshopifyUrl + '/admin/apps'; + } + + StorageAccessHelper.prototype.redirectToAppTargetUrl = function() { + window.location.href = this.redirectData.appTargetUrl; + } + + StorageAccessHelper.prototype.sameSiteNoneIncompatible = function(ua) { + return ua.includes("iPhone OS 12_") || ua.includes("iPad; CPU OS 12_") || //iOS 12 + (ua.includes("UCBrowser/") + ? this.isOlderUcBrowser(ua) //UC Browser < 12.13.2 + : (ua.includes("Chrome/5") || ua.includes("Chrome/6"))) || + ua.includes("Chromium/5") || ua.includes("Chromium/6") || + (ua.includes(" OS X 10_14_") && + ((ua.includes("Version/") && ua.includes("Safari")) || //Safari on MacOS 10.14 + ua.endsWith("(KHTML, like Gecko)"))); //Web view on MacOS 10.14 + } + + StorageAccessHelper.prototype.isOlderUcBrowser = function(ua) { + var match = ua.match(/UCBrowser\\/(\\d+)\\.(\\d+)\\.(\\d+)\\./); + if (!match) return false; + var major = parseInt(match[1]); + var minor = parseInt(match[2]); + var build = parseInt(match[3]); + if (major != 12) return major < 12; + if (minor != 13) return minor < 13; + return build < 2; + } + + StorageAccessHelper.prototype.setCookie = function(value) { + if(!this.sameSiteNoneIncompatible(navigator.userAgent)) { + value += '; secure; SameSite=None' + } + document.cookie = value; + } + + StorageAccessHelper.prototype.grantedStorageAccess = function() { + try { + sessionStorage.setItem('shopify.granted_storage_access', true); + this.setCookie('shopify.granted_storage_access=true'); + if (!document.cookie) { + throw 'Cannot set third-party cookie.' + } + this.redirectToAppTargetUrl(); + } catch (error) { + console.warn('Third party cookies may be blocked.', error); + this.redirectToAppTLD(ACCESS_DENIED_STATUS); + } + } + + StorageAccessHelper.prototype.handleRequestStorageAccess = function() { + return document.requestStorageAccess().then(this.grantedStorageAccess.bind(this), this.redirectToAppsIndex.bind(this, ACCESS_DENIED_STATUS)); + } + + StorageAccessHelper.prototype.setupRequestStorageAccess = function() { + var requestContent = document.getElementById('RequestStorageAccess'); + var requestButton = document.getElementById('TriggerAllowCookiesPrompt'); + + requestButton.addEventListener('click', this.handleRequestStorageAccess.bind(this)); + requestContent.style.display = 'block'; + } + + StorageAccessHelper.prototype.handleHasStorageAccess = function() { + if (sessionStorage.getItem('shopify.granted_storage_access')) { + // If app was classified by ITP and used Storage Access API to acquire access + this.redirectToAppTargetUrl(); + } else { + // If app has not been classified by ITP and still has storage access + this.redirectToAppTLD(ACCESS_GRANTED_STATUS); + } + } + + StorageAccessHelper.prototype.handleGetStorageAccess = function() { + if (sessionStorage.getItem('shopify.top_level_interaction')) { + // If merchant has been redirected to interact with TLD (requirement for prompting request to gain storage access) + this.setupRequestStorageAccess(); + } else { + // If merchant has not been redirected to interact with TLD (requirement for prompting request to gain storage access) + this.redirectToAppTLD(ACCESS_DENIED_STATUS); + } + } + + StorageAccessHelper.prototype.manageStorageAccess = function() { + return document.hasStorageAccess().then(function(hasAccess) { + if (hasAccess) { + this.handleHasStorageAccess(); + } else { + this.handleGetStorageAccess(); + } + }.bind(this)); + } + + StorageAccessHelper.prototype.execute = function() { + if (ITPHelper.prototype.userAgentIsAffected()) { + this.manageStorageAccess(); + } else { + this.grantedStorageAccess(); + } + } + + /* ITP 2.0 solution: handles cookie partitioning */ + StorageAccessHelper.prototype.setUpHelper = function() { + return new ITPHelper({redirectUrl: window.shopOrigin + "/admin/apps/" + window.apiKey + window.returnTo}); + } + + StorageAccessHelper.prototype.setCookieAndRedirect = function() { + this.setCookie('shopify.cookies_persist=true'); + var helper = this.setUpHelper(); + helper.redirect(); + } + + StorageAccessHelper.prototype.setUpCookiePartitioning = function() { + var itpContent = document.getElementById('CookiePartitionPrompt'); + itpContent.style.display = 'block'; + + // var button = document.getElementById('AcceptCookies'); + // button.addEventListener('click', this.setCookieAndRedirect.bind(this)); + } + + this.StorageAccessHelper = StorageAccessHelper; + })(window);`; + +export default storageAccessHelper; diff --git a/packages/koa-shopify-auth/src/auth/client/top-level-interaction.ts b/packages/koa-shopify-auth/src/auth/client/top-level-interaction.ts new file mode 100644 index 0000000000..69984cadd9 --- /dev/null +++ b/packages/koa-shopify-auth/src/auth/client/top-level-interaction.ts @@ -0,0 +1,16 @@ +// Copied from https://github.com/Shopify/shopify_app +const topLevelInteraction = (shop: string) => { + return `(function() { + function setUpTopLevelInteraction() { + var TopLevelInteraction = new ITPHelper({ + redirectUrl: "/auth?shop=${shop}", + }); + + TopLevelInteraction.execute(); + } + + document.addEventListener("DOMContentLoaded", setUpTopLevelInteraction); + })();`; +}; + +export default topLevelInteraction; diff --git a/packages/koa-shopify-auth/src/auth/create-enable-cookies-redirect.ts b/packages/koa-shopify-auth/src/auth/create-enable-cookies-redirect.ts deleted file mode 100644 index 372d5912d5..0000000000 --- a/packages/koa-shopify-auth/src/auth/create-enable-cookies-redirect.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Context} from 'koa'; - -import createTopLevelRedirect from './create-top-level-redirect'; -import getCookieOptions from './cookie-options'; - -import {TEST_COOKIE_NAME} from './index'; - -export default function createEnableCookiesRedirect( - apiKey: string, - path: string, -) { - const redirect = createTopLevelRedirect(apiKey, path); - - return function topLevelOAuthRedirect(ctx: Context) { - // This is to avoid a redirect loop if the app doesn't use verifyRequest or set the test cookie elsewhere. - ctx.cookies.set(TEST_COOKIE_NAME, '1', getCookieOptions(ctx)); - redirect(ctx); - }; -} diff --git a/packages/koa-shopify-auth/src/auth/create-enable-cookies.ts b/packages/koa-shopify-auth/src/auth/create-enable-cookies.ts index 945c88bd68..a21671d730 100644 --- a/packages/koa-shopify-auth/src/auth/create-enable-cookies.ts +++ b/packages/koa-shopify-auth/src/auth/create-enable-cookies.ts @@ -2,6 +2,9 @@ import {Context} from 'koa'; import {OAuthStartOptions} from '../types'; +import css from './client/polaris-css'; +import itpHelper from './client/itp-helper'; +import topLevelInteraction from './client/top-level-interaction'; import Error from './errors'; const HEADING = 'Enable cookies'; @@ -27,339 +30,7 @@ export default function createEnableCookies({apiKey}: OAuthStartOptions) {
${BODY}
+