shopify_app 7.2.0 → 17.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (199) hide show
  1. checksums.yaml +5 -5
  2. data/.babelrc +5 -0
  3. data/.github/CODEOWNERS +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  8. data/.github/probots.yml +2 -0
  9. data/.github/workflows/build.yml +38 -0
  10. data/.github/workflows/release.yml +24 -0
  11. data/.github/workflows/rubocop.yml +22 -0
  12. data/.gitignore +4 -1
  13. data/.nvmrc +1 -0
  14. data/.rubocop.yml +18 -0
  15. data/.ruby-version +1 -0
  16. data/CHANGELOG.md +465 -0
  17. data/CONTRIBUTING.md +76 -0
  18. data/Gemfile +7 -0
  19. data/Gemfile.lock +256 -0
  20. data/README.md +73 -288
  21. data/Rakefile +1 -0
  22. data/SECURITY.md +59 -0
  23. data/app/assets/images/storage_access.svg +1 -0
  24. data/app/assets/javascripts/shopify_app/enable_cookies.js +3 -0
  25. data/app/assets/javascripts/shopify_app/itp_helper.js +40 -0
  26. data/app/assets/javascripts/shopify_app/partition_cookies.js +8 -0
  27. data/app/assets/javascripts/shopify_app/redirect.js +33 -0
  28. data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
  29. data/app/assets/javascripts/shopify_app/storage_access.js +154 -0
  30. data/app/assets/javascripts/shopify_app/storage_access_redirect.js +17 -0
  31. data/app/assets/javascripts/shopify_app/top_level.js +2 -0
  32. data/app/assets/javascripts/shopify_app/top_level_interaction.js +11 -0
  33. data/app/controllers/concerns/shopify_app/authenticated.rb +16 -0
  34. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +26 -0
  35. data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
  36. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
  37. data/app/controllers/shopify_app/authenticated_controller.rb +5 -5
  38. data/app/controllers/shopify_app/callback_controller.rb +196 -0
  39. data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
  40. data/app/controllers/shopify_app/sessions_controller.rb +190 -2
  41. data/app/controllers/shopify_app/webhooks_controller.rb +16 -7
  42. data/app/views/shopify_app/partials/_button_styles.html.erb +109 -0
  43. data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
  44. data/app/views/shopify_app/partials/_empty_state_styles.html.erb +98 -0
  45. data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
  46. data/app/views/shopify_app/partials/_layout_styles.html.erb +182 -0
  47. data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
  48. data/app/views/shopify_app/sessions/enable_cookies.html.erb +70 -0
  49. data/app/views/shopify_app/sessions/new.html.erb +39 -83
  50. data/app/views/shopify_app/sessions/request_storage_access.html.erb +68 -0
  51. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +63 -0
  52. data/app/views/shopify_app/shared/redirect.html.erb +23 -0
  53. data/config/locales/cs.yml +23 -0
  54. data/config/locales/da.yml +20 -0
  55. data/config/locales/de.yml +22 -0
  56. data/config/locales/en.yml +12 -1
  57. data/config/locales/es.yml +21 -3
  58. data/config/locales/fi.yml +20 -0
  59. data/config/locales/fr.yml +23 -0
  60. data/config/locales/hi.yml +23 -0
  61. data/config/locales/it.yml +21 -0
  62. data/config/locales/ja.yml +17 -0
  63. data/config/locales/ko.yml +19 -0
  64. data/config/locales/ms.yml +22 -0
  65. data/config/locales/nb.yml +21 -0
  66. data/config/locales/nl.yml +21 -0
  67. data/config/locales/pl.yml +21 -0
  68. data/config/locales/pt-BR.yml +21 -0
  69. data/config/locales/pt-PT.yml +22 -0
  70. data/config/locales/sv.yml +21 -0
  71. data/config/locales/th.yml +20 -0
  72. data/config/locales/tr.yml +22 -0
  73. data/config/locales/vi.yml +22 -0
  74. data/config/locales/zh-CN.yml +16 -0
  75. data/config/locales/zh-TW.yml +16 -0
  76. data/config/routes.rb +12 -1
  77. data/docs/Quickstart.md +31 -0
  78. data/docs/Releasing.md +21 -0
  79. data/docs/Troubleshooting.md +16 -0
  80. data/docs/Upgrading.md +110 -0
  81. data/docs/shopify_app/authentication.md +124 -0
  82. data/docs/shopify_app/engine.md +82 -0
  83. data/docs/shopify_app/generators.md +127 -0
  84. data/docs/shopify_app/handling-access-scopes-changes.md +8 -0
  85. data/docs/shopify_app/script-tags.md +28 -0
  86. data/docs/shopify_app/session-repository.md +88 -0
  87. data/docs/shopify_app/testing.md +38 -0
  88. data/docs/shopify_app/webhooks.md +72 -0
  89. data/karma.conf.js +44 -0
  90. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +47 -0
  91. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +11 -0
  92. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
  93. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
  94. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
  95. data/lib/generators/shopify_app/add_webhook/templates/{webhook_job.rb → webhook_job.rb.tt} +5 -0
  96. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
  97. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
  98. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
  99. data/lib/generators/shopify_app/app_proxy_controller/templates/index.html.erb +2 -2
  100. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
  101. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
  102. data/lib/generators/shopify_app/controllers/controllers_generator.rb +2 -1
  103. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +31 -9
  104. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +6 -1
  105. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +70 -6
  106. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +11 -0
  107. data/lib/generators/shopify_app/install/install_generator.rb +78 -27
  108. data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +1 -13
  109. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +12 -11
  110. data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
  111. data/lib/generators/shopify_app/install/templates/omniauth.rb +3 -1
  112. data/lib/generators/shopify_app/install/templates/session_store.rb +4 -0
  113. data/lib/generators/shopify_app/install/templates/shopify_app.js +15 -0
  114. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +25 -0
  115. data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
  116. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
  117. data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
  118. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
  119. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
  120. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +16 -0
  121. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +17 -0
  122. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +42 -0
  123. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
  124. data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
  125. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +42 -14
  126. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  127. data/lib/generators/shopify_app/shop_model/templates/db/migrate/{create_shops.rb → create_shops.erb} +1 -1
  128. data/lib/generators/shopify_app/shop_model/templates/shop.rb +6 -2
  129. data/lib/generators/shopify_app/shopify_app_generator.rb +5 -3
  130. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  131. data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
  132. data/lib/generators/shopify_app/user_model/templates/user.rb +8 -0
  133. data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
  134. data/lib/generators/shopify_app/user_model/user_model_generator.rb +70 -0
  135. data/lib/generators/shopify_app/views/views_generator.rb +2 -1
  136. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  137. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  138. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  139. data/lib/shopify_app/configuration.rb +69 -5
  140. data/lib/shopify_app/{app_proxy_verification.rb → controller_concerns/app_proxy_verification.rb} +4 -9
  141. data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
  142. data/lib/shopify_app/controller_concerns/embedded_app.rb +20 -0
  143. data/lib/shopify_app/controller_concerns/itp.rb +45 -0
  144. data/lib/shopify_app/controller_concerns/localization.rb +23 -0
  145. data/lib/shopify_app/controller_concerns/login_protection.rb +244 -0
  146. data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
  147. data/lib/shopify_app/controller_concerns/webhook_verification.rb +23 -0
  148. data/lib/shopify_app/engine.rb +40 -0
  149. data/lib/shopify_app/jobs/scripttags_manager_job.rb +16 -0
  150. data/lib/shopify_app/{webhooks_manager_job.rb → jobs/webhooks_manager_job.rb} +3 -2
  151. data/lib/shopify_app/{scripttags_manager.rb → managers/scripttags_manager.rb} +25 -8
  152. data/lib/shopify_app/{webhooks_manager.rb → managers/webhooks_manager.rb} +6 -5
  153. data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
  154. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
  155. data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
  156. data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
  157. data/lib/shopify_app/session/in_memory_shop_session_store.rb +16 -0
  158. data/lib/shopify_app/session/in_memory_user_session_store.rb +16 -0
  159. data/lib/shopify_app/session/jwt.rb +63 -0
  160. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  161. data/lib/shopify_app/session/session_repository.rb +56 -0
  162. data/lib/shopify_app/session/session_storage.rb +20 -0
  163. data/lib/shopify_app/session/shop_session_storage.rb +42 -0
  164. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
  165. data/lib/shopify_app/session/user_session_storage.rb +42 -0
  166. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
  167. data/lib/shopify_app/test_helpers/all.rb +2 -0
  168. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
  169. data/lib/shopify_app/utils.rb +24 -4
  170. data/lib/shopify_app/version.rb +2 -1
  171. data/lib/shopify_app.rb +65 -24
  172. data/package.json +27 -0
  173. data/service.yml +7 -0
  174. data/shipit.rubygems.yml +3 -0
  175. data/shopify_app.gemspec +20 -9
  176. data/translation.yml +7 -0
  177. data/webpack.config.js +24 -0
  178. data/yarn.lock +5215 -0
  179. metadata +274 -43
  180. data/.travis.yml +0 -17
  181. data/Gemfile.rails50 +0 -5
  182. data/Gemfile.ruby22 +0 -6
  183. data/Gemfile.ruby22.rails50 +0 -9
  184. data/ISSUE_TEMPLATE.md +0 -14
  185. data/QUICKSTART.md +0 -72
  186. data/RELEASING +0 -13
  187. data/lib/generators/shopify_app/home_controller/templates/shopify_app_ready_script.html.erb +0 -11
  188. data/lib/generators/shopify_app/install/templates/shopify_app.rb +0 -9
  189. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -4
  190. data/lib/generators/shopify_app/install/templates/shopify_session_repository.rb +0 -23
  191. data/lib/generators/shopify_app/shop_model/templates/shopify_session_repository.rb +0 -7
  192. data/lib/shopify_app/in_memory_session_store.rb +0 -25
  193. data/lib/shopify_app/login_protection.rb +0 -103
  194. data/lib/shopify_app/scripttags_manager_job.rb +0 -15
  195. data/lib/shopify_app/session_storage.rb +0 -23
  196. data/lib/shopify_app/sessions_concern.rb +0 -101
  197. data/lib/shopify_app/shop.rb +0 -15
  198. data/lib/shopify_app/shopify_session_repository.rb +0 -34
  199. data/lib/shopify_app/webhook_verification.rb +0 -39
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bundler/gem_tasks'
2
3
  require 'rake/testtask'
3
4
 
data/SECURITY.md ADDED
@@ -0,0 +1,59 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ ### New features
6
+
7
+ New features will only be added to the master branch and will not be made available in point releases.
8
+
9
+ ### Bug fixes
10
+
11
+ Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from.
12
+
13
+ ### Security issues
14
+
15
+ Only the latest release series will receive patches and new versions in case of a security issue.
16
+
17
+ ### Severe security issues
18
+
19
+ For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team.
20
+
21
+ ### Unsupported Release Series
22
+
23
+ When a release series is no longer supported, it's your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.
24
+
25
+ ## Reporting a bug
26
+
27
+ All security bugs in shopify repositories should be reported to [our hackerone program](https://hackerone.com/shopify)
28
+ Shopify's whitehat program is our way to reward security researchers for finding serious security vulnerabilities in the In Scope properties listed at the bottom of this page, including our core application (all functionality associated with a Shopify store, particularly your-store.myshopify.com/admin) and certain ancillary applications.
29
+
30
+ ## Disclosure Policy
31
+
32
+ We look forward to working with all security researchers and strive to be respectful, always assume the best and treat others as peers. We expect the same in return from all participants. To achieve this, our team strives to:
33
+
34
+ - Reply to all reports within one business day and triage within two business days (if applicable)
35
+ - Be as transparent as possible, answering all inquires about our report decisions and adding hackers to duplicate HackerOne reports
36
+ - Award bounties within a week of resolution (excluding extenuating circumstances)
37
+ - Only close reports as N/A when the issue reported is included in Known Issues, Ineligible Vulnerabilities Types or lacks evidence of a vulnerability
38
+
39
+ **The following rules must be followed in order for any rewards to be paid:**
40
+
41
+ - You may only test against shops you have created which include your HackerOne YOURHANDLE @ wearehackerone.com registered email address.
42
+ - You must not attempt to gain access to, or interact with, any shops other than those created by you.
43
+ - The use of commercial scanners is prohibited (e.g., Nessus).
44
+ - Rules for reporting must be followed.
45
+ - Do not disclose any issues publicly before they have been resolved.
46
+ - Shopify reserves the right to modify the rules for this program or deem any submissions invalid at any time. Shopify may cancel the whitehat program without notice at any time.
47
+ - Contacting Shopify Support over chat, email or phone about your HackerOne report is not allowed. We may disqualify you from receiving a reward, or from participating in the program altogether.
48
+ - You are not an employee of Shopify; employees should report bugs to the internal bug bounty program.
49
+ - You hereby represent, warrant and covenant that any content you submit to Shopify is an original work of authorship and that you are legally entitled to grant the rights and privileges conveyed by these terms. You further represent, warrant and covenant that the consent of no other person or entity is or will be necessary for Shopify to use the submitted content.
50
+ - By submitting content to Shopify, you irrevocably waive all moral rights which you may have in the content.
51
+ - All content submitted by you to Shopify under this program is licensed under the MIT License.
52
+ - You must report any discovered vulnerability to Shopify as soon as you have validated the vulnerability.
53
+ - Failure to follow any of the foregoing rules will disqualify you from participating in this program.
54
+
55
+ ** Please see our [Hackerone Profile](https://hackerone.com/shopify) for full details
56
+
57
+ ## Receiving Security Updates
58
+
59
+ To recieve all general updates to vulnerabilities, please subscribe to our hackerone [Hacktivity](https://hackerone.com/shopify/hacktivity)
@@ -0,0 +1 @@
1
+ <svg width="140" height="140" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M79 55a9 9 0 00-18 0v8h18v-8zm6 8v-8a15 15 0 00-30 0v8h-5a2 2 0 00-2 2v20a15 15 0 0015 15h14a15 15 0 0015-15V65a2 2 0 00-2-2h-5zM70 90a3 3 0 01-3-3V75a3 3 0 116 0v12a3 3 0 01-3 3z" fill="#8C9196"/></svg>
@@ -0,0 +1,3 @@
1
+ //= require ./itp_helper.js
2
+ //= require ./storage_access.js
3
+ //= require ./partition_cookies.js
@@ -0,0 +1,40 @@
1
+ (function() {
2
+ function ITPHelper(opts) {
3
+ this.itpContent = document.getElementById('TopLevelInteractionContent');
4
+ this.itpAction = document.getElementById('TopLevelInteractionButton');
5
+ this.redirectUrl = opts.redirectUrl;
6
+ }
7
+
8
+ ITPHelper.prototype.redirect = function() {
9
+ sessionStorage.setItem('shopify.top_level_interaction', true);
10
+ window.location.href = this.redirectUrl;
11
+ }
12
+
13
+ ITPHelper.prototype.userAgentIsAffected = function() {
14
+ return Boolean(document.hasStorageAccess);
15
+ }
16
+
17
+ ITPHelper.prototype.canPartitionCookies = function() {
18
+ var versionRegEx = /Version\/12\.0\.?\d? Safari/;
19
+ return versionRegEx.test(navigator.userAgent);
20
+ }
21
+
22
+ ITPHelper.prototype.setUpContent = function(onClick) {
23
+ this.itpContent.style.display = 'block';
24
+ this.itpAction.addEventListener('click', this.redirect.bind(this));
25
+ }
26
+
27
+ ITPHelper.prototype.execute = function() {
28
+ if (!this.itpContent) {
29
+ return;
30
+ }
31
+
32
+ if (this.userAgentIsAffected()) {
33
+ this.setUpContent();
34
+ } else {
35
+ this.redirect();
36
+ }
37
+ }
38
+
39
+ this.ITPHelper = ITPHelper;
40
+ })(window);
@@ -0,0 +1,8 @@
1
+ (function() {
2
+ document.addEventListener("DOMContentLoaded", function() {
3
+ var redirectTargetElement = document.getElementById("redirection-target");
4
+ var targetInfo = JSON.parse(redirectTargetElement.dataset.target)
5
+ var storageAccessHelper = new StorageAccessHelper(targetInfo);
6
+ storageAccessHelper.execute();
7
+ });
8
+ })();
@@ -0,0 +1,33 @@
1
+ (function() {
2
+ function redirect() {
3
+ var redirectTargetElement = document.getElementById("redirection-target");
4
+
5
+ if (!redirectTargetElement) {
6
+ return;
7
+ }
8
+
9
+ var targetInfo = JSON.parse(redirectTargetElement.dataset.target)
10
+
11
+ if (window.top == window.self) {
12
+ // If the current window is the 'parent', change the URL by setting location.href
13
+ window.top.location.href = targetInfo.url;
14
+ } else {
15
+ // If the current window is the 'child', change the parent's URL with postMessage
16
+ normalizedLink = document.createElement('a');
17
+ normalizedLink.href = targetInfo.url;
18
+
19
+ data = JSON.stringify({
20
+ message: 'Shopify.API.remoteRedirect',
21
+ data: {location: normalizedLink.href}
22
+ });
23
+ window.parent.postMessage(data, targetInfo.myshopifyUrl);
24
+ }
25
+ }
26
+
27
+ document.addEventListener("DOMContentLoaded", redirect);
28
+
29
+ // In the turbolinks context, neither DOMContentLoaded nor turbolinks:load
30
+ // consistently fires. This ensures that we at least attempt to fire in the
31
+ // turbolinks situation as well.
32
+ redirect();
33
+ })();
@@ -0,0 +1,3 @@
1
+ //= require ./itp_helper.js
2
+ //= require ./storage_access.js
3
+ //= require ./storage_access_redirect.js
@@ -0,0 +1,154 @@
1
+ (function() {
2
+ var ACCESS_GRANTED_STATUS = 'storage_access_granted';
3
+ var ACCESS_DENIED_STATUS = 'storage_access_denied';
4
+
5
+ function StorageAccessHelper(redirectData) {
6
+ this.redirectData = redirectData;
7
+ }
8
+
9
+ StorageAccessHelper.prototype.setNormalizedLink = function(storageAccessStatus) {
10
+ return storageAccessStatus === ACCESS_GRANTED_STATUS ? this.redirectData.hasStorageAccessUrl : this.redirectData.doesNotHaveStorageAccessUrl;
11
+ }
12
+
13
+ StorageAccessHelper.prototype.redirectToAppTLD = function(storageAccessStatus) {
14
+ var normalizedLink = document.createElement('a');
15
+
16
+ normalizedLink.href = this.setNormalizedLink(storageAccessStatus);
17
+
18
+ data = JSON.stringify({
19
+ message: 'Shopify.API.remoteRedirect',
20
+ data: {
21
+ location: normalizedLink.href,
22
+ }
23
+ });
24
+ window.parent.postMessage(data, this.redirectData.myshopifyUrl);
25
+ }
26
+
27
+ StorageAccessHelper.prototype.redirectToAppsIndex = function() {
28
+ window.parent.location.href = this.redirectData.myshopifyUrl + '/admin/apps';
29
+ }
30
+
31
+ StorageAccessHelper.prototype.redirectToAppTargetUrl = function() {
32
+ window.location.href = this.redirectData.appTargetUrl;
33
+ }
34
+
35
+ StorageAccessHelper.prototype.sameSiteNoneIncompatible = function(ua) {
36
+ return ua.includes("iPhone OS 12_") || ua.includes("iPad; CPU OS 12_") || //iOS 12
37
+ (ua.includes("UCBrowser/")
38
+ ? this.isOlderUcBrowser(ua) //UC Browser < 12.13.2
39
+ : (ua.includes("Chrome/5") || ua.includes("Chrome/6"))) ||
40
+ ua.includes("Chromium/5") || ua.includes("Chromium/6") ||
41
+ (ua.includes(" OS X 10_14_") &&
42
+ ((ua.includes("Version/") && ua.includes("Safari")) || //Safari on MacOS 10.14
43
+ ua.endsWith("(KHTML, like Gecko)"))); //Web view on MacOS 10.14
44
+ }
45
+
46
+ StorageAccessHelper.prototype.isOlderUcBrowser = function(ua) {
47
+ var match = ua.match(/UCBrowser\/(\d+)\.(\d+)\.(\d+)\./);
48
+ if (!match) return false;
49
+ var major = parseInt(match[1]);
50
+ var minor = parseInt(match[2]);
51
+ var build = parseInt(match[3]);
52
+ if (major != 12) return major < 12;
53
+ if (minor != 13) return minor < 13;
54
+ return build < 2;
55
+ }
56
+
57
+ StorageAccessHelper.prototype.setCookie = function(value) {
58
+ if(!this.sameSiteNoneIncompatible(navigator.userAgent)) {
59
+ value += '; secure; SameSite=None'
60
+ }
61
+ document.cookie = value;
62
+ }
63
+
64
+ StorageAccessHelper.prototype.grantedStorageAccess = function() {
65
+ try {
66
+ sessionStorage.setItem('shopify.granted_storage_access', true);
67
+ this.setCookie('shopify.granted_storage_access=true');
68
+ if (!document.cookie) {
69
+ throw 'Cannot set third-party cookie.'
70
+ }
71
+ this.redirectToAppTargetUrl();
72
+ } catch (error) {
73
+ console.warn('Third party cookies may be blocked.', error);
74
+ this.redirectToAppTLD(ACCESS_DENIED_STATUS);
75
+ }
76
+ }
77
+
78
+ StorageAccessHelper.prototype.handleRequestStorageAccess = function() {
79
+ return document.requestStorageAccess().then(this.grantedStorageAccess.bind(this), this.redirectToAppsIndex.bind(this, ACCESS_DENIED_STATUS));
80
+ }
81
+
82
+ StorageAccessHelper.prototype.setupRequestStorageAccess = function() {
83
+ var requestContent = document.getElementById('RequestStorageAccess');
84
+ var requestButton = document.getElementById('TriggerAllowCookiesPrompt');
85
+
86
+ requestButton.addEventListener('click', this.handleRequestStorageAccess.bind(this));
87
+ requestContent.style.display = 'block';
88
+ }
89
+
90
+ StorageAccessHelper.prototype.handleHasStorageAccess = function() {
91
+ if (sessionStorage.getItem('shopify.granted_storage_access')) {
92
+ // If app was classified by ITP and used Storage Access API to acquire access
93
+ this.redirectToAppTargetUrl();
94
+ } else {
95
+ // If app has not been classified by ITP and still has storage access
96
+ this.redirectToAppTLD(ACCESS_GRANTED_STATUS);
97
+ }
98
+ }
99
+
100
+ StorageAccessHelper.prototype.handleGetStorageAccess = function() {
101
+ if (sessionStorage.getItem('shopify.top_level_interaction')) {
102
+ // If merchant has been redirected to interact with TLD (requirement for prompting request to gain storage access)
103
+ this.setupRequestStorageAccess();
104
+ } else {
105
+ // If merchant has not been redirected to interact with TLD (requirement for prompting request to gain storage access)
106
+ this.redirectToAppTLD(ACCESS_DENIED_STATUS);
107
+ }
108
+ }
109
+
110
+ StorageAccessHelper.prototype.manageStorageAccess = function() {
111
+ return document.hasStorageAccess().then(function(hasAccess) {
112
+ if (hasAccess) {
113
+ this.handleHasStorageAccess();
114
+ } else {
115
+ this.handleGetStorageAccess();
116
+ }
117
+ }.bind(this));
118
+ }
119
+
120
+ StorageAccessHelper.prototype.execute = function() {
121
+ if (ITPHelper.prototype.canPartitionCookies()) {
122
+ this.setUpCookiePartitioning();
123
+ return;
124
+ }
125
+
126
+ if (ITPHelper.prototype.userAgentIsAffected()) {
127
+ this.manageStorageAccess();
128
+ } else {
129
+ this.grantedStorageAccess();
130
+ }
131
+ }
132
+
133
+ /* ITP 2.0 solution: handles cookie partitioning */
134
+ StorageAccessHelper.prototype.setUpHelper = function() {
135
+ var shopifyData = document.body.dataset;
136
+ return new ITPHelper({redirectUrl: shopifyData.shopOrigin + "/admin/apps/" + shopifyData.apiKey + shopifyData.returnTo});
137
+ }
138
+
139
+ StorageAccessHelper.prototype.setCookieAndRedirect = function() {
140
+ this.setCookie('shopify.cookies_persist=true');
141
+ var helper = this.setUpHelper();
142
+ helper.redirect();
143
+ }
144
+
145
+ StorageAccessHelper.prototype.setUpCookiePartitioning = function() {
146
+ var itpContent = document.getElementById('CookiePartitionPrompt');
147
+ itpContent.style.display = 'block';
148
+
149
+ var button = document.getElementById('AcceptCookies');
150
+ button.addEventListener('click', this.setCookieAndRedirect.bind(this));
151
+ }
152
+
153
+ this.StorageAccessHelper = StorageAccessHelper;
154
+ })(window);
@@ -0,0 +1,17 @@
1
+ (function() {
2
+ function redirect() {
3
+ var redirectTargetElement = document.getElementById("redirection-target");
4
+
5
+ var targetInfo = JSON.parse(redirectTargetElement.dataset.target)
6
+
7
+ if (window.top == window.self) {
8
+ // If the current window is the 'parent', change the URL by setting location.href
9
+ window.top.location.href = targetInfo.hasStorageAccessUrl;
10
+ } else {
11
+ var storageAccessHelper = new StorageAccessHelper(targetInfo);
12
+ storageAccessHelper.execute();
13
+ }
14
+ }
15
+
16
+ document.addEventListener("DOMContentLoaded", redirect);
17
+ })();
@@ -0,0 +1,2 @@
1
+ //= require ./itp_helper.js
2
+ //= require ./top_level_interaction.js
@@ -0,0 +1,11 @@
1
+ (function() {
2
+ function setUpTopLevelInteraction() {
3
+ var TopLevelInteraction = new ITPHelper({
4
+ redirectUrl: document.body.dataset.redirectUrl,
5
+ });
6
+
7
+ TopLevelInteraction.execute();
8
+ }
9
+
10
+ document.addEventListener("DOMContentLoaded", setUpTopLevelInteraction);
11
+ })();
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Authenticated
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ShopifyApp::Localization
9
+ include ShopifyApp::LoginProtection
10
+ include ShopifyApp::CsrfProtection
11
+ include ShopifyApp::EmbeddedApp
12
+ before_action :login_again_if_different_user_or_shop
13
+ around_action :activate_shopify_session
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module EnsureAuthenticatedLinks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :redirect_to_splash_page, if: :missing_expected_jwt?
9
+ end
10
+
11
+ private
12
+
13
+ def redirect_to_splash_page
14
+ splash_page_path = root_path(return_to: request.fullpath, shop: current_shopify_domain)
15
+ redirect_to(splash_page_path)
16
+ rescue ShopifyApp::LoginProtection::ShopifyDomainNotFound => error
17
+ Rails.logger.warn("[ShopifyApp::EnsureAuthenticatedLinks] Redirecting to login: [#{error.class}] "\
18
+ "Could not determine current shop domain")
19
+ redirect_to(ShopifyApp.configuration.login_url)
20
+ end
21
+
22
+ def missing_expected_jwt?
23
+ jwt_shopify_domain.blank?
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module RequireKnownShop
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :check_shop_domain
9
+ before_action :check_shop_known
10
+ end
11
+
12
+ def current_shopify_domain
13
+ return if params[:shop].blank?
14
+ @shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
15
+ end
16
+
17
+ private
18
+
19
+ def check_shop_domain
20
+ redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
21
+ end
22
+
23
+ def check_shop_known
24
+ @shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
25
+ redirect_to(shop_login) unless @shop
26
+ end
27
+
28
+ def shop_login
29
+ url = URI(ShopifyApp.configuration.login_url)
30
+
31
+ url.query = URI.encode_www_form(
32
+ shop: params[:shop],
33
+ return_to: request.fullpath,
34
+ )
35
+
36
+ url.to_s
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module ShopAccessScopesVerification
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :login_on_scope_changes
9
+ end
10
+
11
+ protected
12
+
13
+ def login_on_scope_changes
14
+ redirect_to(shop_login) if scopes_mismatch?
15
+ end
16
+
17
+ private
18
+
19
+ def scopes_mismatch?
20
+ ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(current_shopify_domain)
21
+ end
22
+
23
+ def current_shopify_domain
24
+ return if params[:shop].blank?
25
+ ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
26
+ end
27
+
28
+ def shop_login
29
+ ShopifyApp::Utils.shop_login_url(shop: params[:shop], return_to: request.fullpath)
30
+ end
31
+ end
32
+ end
@@ -1,8 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
- class AuthenticatedController < ApplicationController
3
- include ShopifyApp::LoginProtection
4
- before_action :login_again_if_different_shop
5
- around_action :shopify_session
6
- layout ShopifyApp.configuration.embedded_app? ? 'embedded_app' : 'application'
3
+ class AuthenticatedController < ActionController::Base
4
+ include ShopifyApp::Authenticated
5
+
6
+ protect_from_forgery with: :exception
7
7
  end
8
8
  end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ # Performs login after OAuth completes
5
+ class CallbackController < ActionController::Base
6
+ include ShopifyApp::LoginProtection
7
+
8
+ def callback
9
+ return respond_with_error if invalid_request?
10
+
11
+ store_access_token_and_build_session
12
+
13
+ if start_user_token_flow?
14
+ return respond_with_user_token_flow
15
+ end
16
+
17
+ perform_post_authenticate_jobs
18
+
19
+ respond_successfully
20
+ end
21
+
22
+ private
23
+
24
+ def respond_successfully
25
+ if jwt_request?
26
+ head(:ok)
27
+ else
28
+ redirect_to(return_address)
29
+ end
30
+ end
31
+
32
+ def respond_with_user_token_flow
33
+ redirect_to(login_url_with_optional_shop)
34
+ end
35
+
36
+ def store_access_token_and_build_session
37
+ if native_browser_request?
38
+ reset_session_options
39
+ end
40
+ set_shopify_session
41
+ end
42
+
43
+ def invalid_request?
44
+ return true unless auth_hash
45
+
46
+ jwt_request? && !valid_jwt_auth?
47
+ end
48
+
49
+ def native_browser_request?
50
+ !jwt_request?
51
+ end
52
+
53
+ def perform_post_authenticate_jobs
54
+ install_webhooks
55
+ install_scripttags
56
+ perform_after_authenticate_job
57
+ end
58
+
59
+ def respond_with_error
60
+ if jwt_request?
61
+ head(:unauthorized)
62
+ else
63
+ flash[:error] = I18n.t('could_not_log_in')
64
+ redirect_to(login_url_with_optional_shop)
65
+ end
66
+ end
67
+
68
+ # Override user_session_by_cookie from LoginProtection to bypass allow_cookie_authentication
69
+ # setting check because session cookies are justified at top level
70
+ def user_session_by_cookie
71
+ return unless session[:user_id].present?
72
+ ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
73
+ end
74
+
75
+ def start_user_token_flow?
76
+ if jwt_request?
77
+ false
78
+ else
79
+ return false unless ShopifyApp::SessionRepository.user_storage.present?
80
+ update_user_access_scopes?
81
+ end
82
+ end
83
+
84
+ def update_user_access_scopes?
85
+ return true if user_session.blank?
86
+ user_access_scopes_strategy.update_access_scopes?(user_id: session[:user_id])
87
+ end
88
+
89
+ def user_access_scopes_strategy
90
+ ShopifyApp.configuration.user_access_scopes_strategy
91
+ end
92
+
93
+ def jwt_request?
94
+ jwt_shopify_domain || jwt_shopify_user_id
95
+ end
96
+
97
+ def valid_jwt_auth?
98
+ auth_hash && jwt_shopify_domain == shop_name && jwt_shopify_user_id == associated_user_id
99
+ end
100
+
101
+ def auth_hash
102
+ request.env['omniauth.auth']
103
+ end
104
+
105
+ def shop_name
106
+ auth_hash.uid
107
+ end
108
+
109
+ def offline_access_token
110
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop_name)&.token
111
+ end
112
+
113
+ def online_access_token
114
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(associated_user_id)&.token
115
+ end
116
+
117
+ def associated_user
118
+ return unless auth_hash.dig('extra', 'associated_user').present?
119
+
120
+ auth_hash['extra']['associated_user'].merge('scope' => auth_hash['extra']['associated_user_scope'])
121
+ end
122
+
123
+ def associated_user_id
124
+ associated_user && associated_user['id']
125
+ end
126
+
127
+ def token
128
+ auth_hash['credentials']['token']
129
+ end
130
+
131
+ def access_scopes
132
+ return unless auth_hash['extra']['scope']
133
+ auth_hash['extra']['scope']
134
+ end
135
+
136
+ def reset_session_options
137
+ request.session_options[:renew] = true
138
+ session.delete(:_csrf_token)
139
+ end
140
+
141
+ def set_shopify_session
142
+ session_store = ShopifyAPI::Session.new(
143
+ domain: shop_name,
144
+ token: token,
145
+ api_version: ShopifyApp.configuration.api_version,
146
+ access_scopes: access_scopes
147
+ )
148
+
149
+ session[:shopify_user] = associated_user
150
+ if session[:shopify_user].present?
151
+ session[:shop_id] = nil if shop_session && shop_session.domain != shop_name
152
+ session[:user_id] = ShopifyApp::SessionRepository.store_user_session(session_store, associated_user)
153
+ else
154
+ session[:shop_id] = ShopifyApp::SessionRepository.store_shop_session(session_store)
155
+ session[:user_id] = nil if user_session && user_session.domain != shop_name
156
+ end
157
+ session[:shopify_domain] = shop_name
158
+ session[:user_session] = auth_hash&.extra&.session
159
+ end
160
+
161
+ def install_webhooks
162
+ return unless ShopifyApp.configuration.has_webhooks?
163
+
164
+ WebhooksManager.queue(
165
+ shop_name,
166
+ offline_access_token || online_access_token,
167
+ ShopifyApp.configuration.webhooks
168
+ )
169
+ end
170
+
171
+ def install_scripttags
172
+ return unless ShopifyApp.configuration.has_scripttags?
173
+
174
+ ScripttagsManager.queue(
175
+ shop_name,
176
+ offline_access_token || online_access_token,
177
+ ShopifyApp.configuration.scripttags
178
+ )
179
+ end
180
+
181
+ def perform_after_authenticate_job
182
+ config = ShopifyApp.configuration.after_authenticate_job
183
+
184
+ return unless config && config[:job].present?
185
+
186
+ job = config[:job]
187
+ job = job.constantize if job.is_a?(String)
188
+
189
+ if config[:inline] == true
190
+ job.perform_now(shop_domain: session[:shopify_domain])
191
+ else
192
+ job.perform_later(shop_domain: session[:shopify_domain])
193
+ end
194
+ end
195
+ end
196
+ end