shopify_app 18.0.2 → 19.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +4 -5
  3. data/.gitignore +1 -0
  4. data/.nvmrc +1 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +52 -2
  7. data/CONTRIBUTING.md +6 -1
  8. data/Gemfile +3 -2
  9. data/Gemfile.lock +144 -164
  10. data/README.md +1 -0
  11. data/Rakefile +4 -3
  12. data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +10 -0
  13. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
  14. data/app/assets/javascripts/shopify_app/redirect.js +9 -11
  15. data/app/assets/javascripts/shopify_app/storage_access.js +4 -10
  16. data/app/controllers/concerns/shopify_app/authenticated.rb +3 -0
  17. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +16 -3
  18. data/app/controllers/concerns/shopify_app/require_known_shop.rb +1 -0
  19. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +1 -1
  20. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  21. data/app/controllers/shopify_app/callback_controller.rb +49 -134
  22. data/app/controllers/shopify_app/sessions_controller.rb +26 -131
  23. data/app/controllers/shopify_app/webhooks_controller.rb +5 -24
  24. data/app/views/shopify_app/sessions/enable_cookies.html.erb +1 -1
  25. data/app/views/shopify_app/sessions/request_storage_access.html.erb +11 -11
  26. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +1 -1
  27. data/app/views/shopify_app/shared/redirect.html.erb +2 -2
  28. data/config/locales/zh-CN.yml +1 -1
  29. data/config/routes.rb +20 -12
  30. data/docs/Troubleshooting.md +0 -3
  31. data/docs/Upgrading.md +116 -14
  32. data/docs/shopify_app/engine.md +2 -2
  33. data/docs/shopify_app/handling-access-scopes-changes.md +11 -1
  34. data/docs/shopify_app/script-tags.md +1 -1
  35. data/docs/shopify_app/webhooks.md +3 -3
  36. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +10 -9
  37. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  38. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +4 -3
  39. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +13 -12
  40. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +9 -1
  41. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +7 -6
  42. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +2 -1
  43. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +1 -1
  44. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +3 -3
  45. data/lib/generators/shopify_app/controllers/controllers_generator.rb +4 -3
  46. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +11 -15
  47. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +2 -2
  48. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +7 -3
  49. data/lib/generators/shopify_app/install/install_generator.rb +27 -72
  50. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +3 -1
  51. data/lib/generators/shopify_app/install/templates/session_store.rb +2 -1
  52. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +33 -5
  53. data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
  54. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +3 -3
  55. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +1 -1
  56. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +4 -4
  57. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -0
  58. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  59. data/lib/generators/shopify_app/routes/routes_generator.rb +6 -5
  60. data/lib/generators/shopify_app/routes/templates/routes.rb +5 -5
  61. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +11 -10
  62. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  63. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  64. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
  65. data/lib/generators/shopify_app/user_model/user_model_generator.rb +11 -10
  66. data/lib/generators/shopify_app/views/views_generator.rb +4 -3
  67. data/lib/shopify_app/access_scopes/shop_strategy.rb +2 -2
  68. data/lib/shopify_app/access_scopes/user_strategy.rb +4 -4
  69. data/lib/shopify_app/configuration.rb +33 -14
  70. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +4 -3
  71. data/lib/shopify_app/controller_concerns/csrf_protection.rb +2 -1
  72. data/lib/shopify_app/controller_concerns/embedded_app.rb +4 -3
  73. data/lib/shopify_app/controller_concerns/ensure_billing.rb +254 -0
  74. data/lib/shopify_app/controller_concerns/itp.rb +3 -3
  75. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  76. data/lib/shopify_app/controller_concerns/login_protection.rb +82 -68
  77. data/lib/shopify_app/controller_concerns/payload_verification.rb +3 -2
  78. data/lib/shopify_app/controller_concerns/webhook_verification.rb +2 -1
  79. data/lib/shopify_app/engine.rb +7 -15
  80. data/lib/shopify_app/jobs/scripttags_manager_job.rb +2 -2
  81. data/lib/shopify_app/jobs/webhooks_manager_job.rb +4 -5
  82. data/lib/shopify_app/managers/scripttags_manager.rb +11 -4
  83. data/lib/shopify_app/managers/webhooks_manager.rb +42 -44
  84. data/lib/shopify_app/middleware/jwt_middleware.rb +5 -3
  85. data/lib/shopify_app/session/in_memory_session_store.rb +1 -0
  86. data/lib/shopify_app/session/in_memory_shop_session_store.rb +2 -1
  87. data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -0
  88. data/lib/shopify_app/session/jwt.rb +12 -7
  89. data/lib/shopify_app/session/null_user_session_store.rb +2 -1
  90. data/lib/shopify_app/session/session_repository.rb +37 -0
  91. data/lib/shopify_app/session/session_storage.rb +4 -6
  92. data/lib/shopify_app/session/shop_session_storage.rb +6 -6
  93. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +7 -8
  94. data/lib/shopify_app/session/user_session_storage.rb +19 -6
  95. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +22 -9
  96. data/lib/shopify_app/test_helpers/all.rb +2 -1
  97. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +4 -3
  98. data/lib/shopify_app/utils.rb +4 -10
  99. data/lib/shopify_app/version.rb +2 -1
  100. data/lib/shopify_app.rb +44 -40
  101. data/package.json +1 -1
  102. data/shopify_app.gemspec +22 -21
  103. data/translation.yml +1 -1
  104. data/yarn.lock +103 -88
  105. metadata +51 -60
  106. data/config/locales/hi.yml +0 -23
  107. data/config/locales/ms.yml +0 -22
  108. data/lib/generators/shopify_app/install/templates/omniauth.rb +0 -4
  109. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +0 -8
  110. data/lib/generators/shopify_app/install/templates/user_agent.rb +0 -6
  111. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +0 -34
  112. data/lib/shopify_app/omniauth/omniauth_configuration.rb +0 -64
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class Configuration
4
5
  # Shopify App settings. These values should match the configuration
@@ -23,6 +24,7 @@ module ShopifyApp
23
24
  # customise urls
24
25
  attr_accessor :root_url
25
26
  attr_writer :login_url
27
+ attr_writer :login_callback_url
26
28
 
27
29
  # customise ActiveJob queue names
28
30
  attr_accessor :scripttags_manager_queue_name
@@ -37,25 +39,24 @@ module ShopifyApp
37
39
  # allow namespacing webhook jobs
38
40
  attr_accessor :webhook_jobs_namespace
39
41
 
40
- # allow enabling of same site none on cookies
41
- attr_writer :enable_same_site_none
42
-
43
- # allow enabling jwt headers for authentication
44
- attr_accessor :allow_jwt_authentication
45
-
46
- attr_accessor :allow_cookie_authentication
42
+ # takes a ShopifyApp::BillingConfiguration object
43
+ attr_accessor :billing
47
44
 
48
45
  def initialize
49
- @root_url = '/'
50
- @myshopify_domain = 'myshopify.com'
46
+ @root_url = "/"
47
+ @myshopify_domain = "myshopify.com"
51
48
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
52
49
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
53
- @disable_webpacker = ENV['SHOPIFY_APP_DISABLE_WEBPACKER'].present?
54
- @allow_cookie_authentication = true
50
+ @disable_webpacker = ENV["SHOPIFY_APP_DISABLE_WEBPACKER"].present?
55
51
  end
56
52
 
57
53
  def login_url
58
- @login_url || File.join(@root_url, 'login')
54
+ @login_url || File.join(@root_url, "login")
55
+ end
56
+
57
+ def login_callback_url
58
+ # Not including @root_url to keep historic behaviour
59
+ @login_callback_url || File.join("auth/shopify/callback")
59
60
  end
60
61
 
61
62
  def user_session_repository=(klass)
@@ -92,8 +93,8 @@ module ShopifyApp
92
93
  scripttags.present?
93
94
  end
94
95
 
95
- def enable_same_site_none
96
- !Rails.env.test? && (@enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none)
96
+ def requires_billing?
97
+ billing.present?
97
98
  end
98
99
 
99
100
  def shop_access_scopes
@@ -105,6 +106,24 @@ module ShopifyApp
105
106
  end
106
107
  end
107
108
 
109
+ class BillingConfiguration
110
+ INTERVAL_ONE_TIME = "ONE_TIME"
111
+ INTERVAL_EVERY_30_DAYS = "EVERY_30_DAYS"
112
+ INTERVAL_ANNUAL = "ANNUAL"
113
+
114
+ attr_reader :charge_name
115
+ attr_reader :amount
116
+ attr_reader :currency_code
117
+ attr_reader :interval
118
+
119
+ def initialize(charge_name:, amount:, interval:, currency_code: "USD")
120
+ @charge_name = charge_name
121
+ @amount = amount
122
+ @currency_code = currency_code
123
+ @interval = interval
124
+ end
125
+ end
126
+
108
127
  def self.configuration
109
128
  @configuration ||= Configuration.new
110
129
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module AppProxyVerification
4
5
  extend ActiveSupport::Concern
@@ -16,7 +17,7 @@ module ShopifyApp
16
17
  def query_string_valid?(query_string)
17
18
  query_hash = Rack::Utils.parse_query(query_string)
18
19
 
19
- signature = query_hash.delete('signature')
20
+ signature = query_hash.delete("signature")
20
21
  return false if signature.nil?
21
22
 
22
23
  ActiveSupport::SecurityUtils.secure_compare(
@@ -26,10 +27,10 @@ module ShopifyApp
26
27
  end
27
28
 
28
29
  def calculated_signature(query_hash_without_signature)
29
- sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
30
+ sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(",")}" }.sort.join
30
31
 
31
32
  OpenSSL::HMAC.hexdigest(
32
- OpenSSL::Digest.new('sha256'),
33
+ OpenSSL::Digest.new("sha256"),
33
34
  ShopifyApp.configuration.secret,
34
35
  sorted_params
35
36
  )
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module CsrfProtection
4
5
  extend ActiveSupport::Concern
@@ -9,7 +10,7 @@ module ShopifyApp
9
10
  private
10
11
 
11
12
  def valid_session_token?
12
- request.env['jwt.shopify_domain']
13
+ request.env["jwt.shopify_domain"]
13
14
  end
14
15
  end
15
16
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module EmbeddedApp
4
5
  extend ActiveSupport::Concern
@@ -6,15 +7,15 @@ module ShopifyApp
6
7
  included do
7
8
  if ShopifyApp.configuration.embedded_app?
8
9
  after_action(:set_esdk_headers)
9
- layout('embedded_app')
10
+ layout("embedded_app")
10
11
  end
11
12
  end
12
13
 
13
14
  private
14
15
 
15
16
  def set_esdk_headers
16
- response.set_header('P3P', 'CP="Not used"')
17
- response.headers.except!('X-Frame-Options')
17
+ response.set_header("P3P", 'CP="Not used"')
18
+ response.headers.except!("X-Frame-Options")
18
19
  end
19
20
  end
20
21
  end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module EnsureBilling
5
+ class BillingError < StandardError
6
+ attr_accessor :message
7
+ attr_accessor :errors
8
+
9
+ def initialize(message, errors)
10
+ super
11
+ @message = message
12
+ @errors = errors
13
+ end
14
+ end
15
+
16
+ extend ActiveSupport::Concern
17
+
18
+ RECURRING_INTERVALS = [BillingConfiguration::INTERVAL_EVERY_30_DAYS, BillingConfiguration::INTERVAL_ANNUAL]
19
+
20
+ included do
21
+ before_action :check_billing, if: :billing_required?
22
+ rescue_from BillingError, with: :handle_billing_error
23
+ end
24
+
25
+ private
26
+
27
+ def check_billing(session = current_shopify_session)
28
+ return true if session.blank? || !billing_required?
29
+
30
+ confirmation_url = nil
31
+
32
+ if has_active_payment?(session)
33
+ has_payment = true
34
+ else
35
+ has_payment = false
36
+ confirmation_url = request_payment(session)
37
+ end
38
+
39
+ unless has_payment
40
+ if request.xhr?
41
+ add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true)
42
+ head(:unauthorized)
43
+ else
44
+ redirect_to(confirmation_url, allow_other_host: true)
45
+ end
46
+ end
47
+
48
+ has_payment
49
+ end
50
+
51
+ def billing_required?
52
+ ShopifyApp.configuration.requires_billing?
53
+ end
54
+
55
+ def handle_billing_error(error)
56
+ logger.info("#{error.message}: #{error.errors}")
57
+ redirect_to_login
58
+ end
59
+
60
+ def has_active_payment?(session)
61
+ if recurring?
62
+ has_subscription?(session)
63
+ else
64
+ has_one_time_payment?(session)
65
+ end
66
+ end
67
+
68
+ def has_subscription?(session)
69
+ response = run_query(session: session, query: RECURRING_PURCHASES_QUERY)
70
+ subscriptions = response.body["data"]["currentAppInstallation"]["activeSubscriptions"]
71
+
72
+ subscriptions.each do |subscription|
73
+ if subscription["name"] == ShopifyApp.configuration.billing.charge_name &&
74
+ (!Rails.env.production? || !subscription["test"])
75
+
76
+ return true
77
+ end
78
+ end
79
+
80
+ false
81
+ end
82
+
83
+ def has_one_time_payment?(session)
84
+ purchases = nil
85
+ end_cursor = nil
86
+
87
+ loop do
88
+ response = run_query(session: session, query: ONE_TIME_PURCHASES_QUERY, variables: { endCursor: end_cursor })
89
+ purchases = response.body["data"]["currentAppInstallation"]["oneTimePurchases"]
90
+
91
+ purchases["edges"].each do |purchase|
92
+ node = purchase["node"]
93
+
94
+ if node["name"] == ShopifyApp.configuration.billing.charge_name &&
95
+ (!Rails.env.production? || !node["test"]) &&
96
+ node["status"] == "ACTIVE"
97
+
98
+ return true
99
+ end
100
+ end
101
+
102
+ end_cursor = purchases["pageInfo"]["endCursor"]
103
+ break unless purchases["pageInfo"]["hasNextPage"]
104
+ end
105
+
106
+ false
107
+ end
108
+
109
+ def request_payment(session)
110
+ shop = session.shop
111
+ host = Base64.encode64("#{shop}/admin")
112
+ return_url = "https://#{ShopifyAPI::Context.host_name}?shop=#{shop}&host=#{host}"
113
+
114
+ if recurring?
115
+ data = request_recurring_payment(session: session, return_url: return_url)
116
+ data = data["data"]["appSubscriptionCreate"]
117
+ else
118
+ data = request_one_time_payment(session: session, return_url: return_url)
119
+ data = data["data"]["appPurchaseOneTimeCreate"]
120
+ end
121
+
122
+ raise BillingError.new("Error while billing the store", data["userErrros"]) unless data["userErrors"].empty?
123
+
124
+ data["confirmationUrl"]
125
+ end
126
+
127
+ def request_recurring_payment(session:, return_url:)
128
+ response = run_query(
129
+ session: session,
130
+ query: RECURRING_PURCHASE_MUTATION,
131
+ variables: {
132
+ name: ShopifyApp.configuration.billing.charge_name,
133
+ lineItems: {
134
+ plan: {
135
+ appRecurringPricingDetails: {
136
+ interval: ShopifyApp.configuration.billing.interval,
137
+ price: {
138
+ amount: ShopifyApp.configuration.billing.amount,
139
+ currencyCode: ShopifyApp.configuration.billing.currency_code,
140
+ },
141
+ },
142
+ },
143
+ },
144
+ returnUrl: return_url,
145
+ test: !Rails.env.production?,
146
+ }
147
+ )
148
+
149
+ response.body
150
+ end
151
+
152
+ def request_one_time_payment(session:, return_url:)
153
+ response = run_query(
154
+ session: session,
155
+ query: ONE_TIME_PURCHASE_MUTATION,
156
+ variables: {
157
+ name: ShopifyApp.configuration.billing.charge_name,
158
+ price: {
159
+ amount: ShopifyApp.configuration.billing.amount,
160
+ currencyCode: ShopifyApp.configuration.billing.currency_code,
161
+ },
162
+ returnUrl: return_url,
163
+ test: !Rails.env.production?,
164
+ }
165
+ )
166
+
167
+ response.body
168
+ end
169
+
170
+ def recurring?
171
+ RECURRING_INTERVALS.include?(ShopifyApp.configuration.billing.interval)
172
+ end
173
+
174
+ def run_query(session:, query:, variables: nil)
175
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: session)
176
+
177
+ response = client.query(query: query, variables: variables)
178
+
179
+ raise BillingError.new("Error while billing the store", []) unless response.ok?
180
+ raise BillingError.new("Error while billing the store", response.body["errors"]) if response.body["errors"]
181
+
182
+ response
183
+ end
184
+
185
+ RECURRING_PURCHASES_QUERY = <<~'QUERY'
186
+ query appSubscription {
187
+ currentAppInstallation {
188
+ activeSubscriptions {
189
+ name, test
190
+ }
191
+ }
192
+ }
193
+ QUERY
194
+
195
+ ONE_TIME_PURCHASES_QUERY = <<~'QUERY'
196
+ query appPurchases($endCursor: String) {
197
+ currentAppInstallation {
198
+ oneTimePurchases(first: 250, sortKey: CREATED_AT, after: $endCursor) {
199
+ edges {
200
+ node {
201
+ name, test, status
202
+ }
203
+ }
204
+ pageInfo {
205
+ hasNextPage, endCursor
206
+ }
207
+ }
208
+ }
209
+ }
210
+ QUERY
211
+
212
+ RECURRING_PURCHASE_MUTATION = <<~'QUERY'
213
+ mutation createPaymentMutation(
214
+ $name: String!
215
+ $lineItems: [AppSubscriptionLineItemInput!]!
216
+ $returnUrl: URL!
217
+ $test: Boolean
218
+ ) {
219
+ appSubscriptionCreate(
220
+ name: $name
221
+ lineItems: $lineItems
222
+ returnUrl: $returnUrl
223
+ test: $test
224
+ ) {
225
+ confirmationUrl
226
+ userErrors {
227
+ field, message
228
+ }
229
+ }
230
+ }
231
+ QUERY
232
+
233
+ ONE_TIME_PURCHASE_MUTATION = <<~'QUERY'
234
+ mutation createPaymentMutation(
235
+ $name: String!
236
+ $price: MoneyInput!
237
+ $returnUrl: URL!
238
+ $test: Boolean
239
+ ) {
240
+ appPurchaseOneTimeCreate(
241
+ name: $name
242
+ price: $price
243
+ returnUrl: $returnUrl
244
+ test: $test
245
+ ) {
246
+ confirmationUrl
247
+ userErrors {
248
+ field, message
249
+ }
250
+ }
251
+ }
252
+ QUERY
253
+ end
254
+ end
@@ -9,15 +9,15 @@ module ShopifyApp
9
9
  return unless ShopifyApp.configuration.embedded_app?
10
10
  return unless user_agent_can_partition_cookies
11
11
 
12
- session['shopify.cookies_persist'] = true
12
+ session["shopify.cookies_persist"] = true
13
13
  end
14
14
 
15
15
  def set_top_level_oauth_cookie
16
- session['shopify.top_level_oauth'] = true
16
+ session["shopify.top_level_oauth"] = true
17
17
  end
18
18
 
19
19
  def clear_top_level_oauth_cookie
20
- session.delete('shopify.top_level_oauth')
20
+ session.delete("shopify.top_level_oauth")
21
21
  end
22
22
 
23
23
  def user_agent_is_mobile
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module Localization
4
5
  extend ActiveSupport::Concern
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'browser_sniffer'
3
+ require "browser_sniffer"
4
4
 
5
5
  module ShopifyApp
6
6
  module LoginProtection
@@ -13,106 +13,99 @@ module ShopifyApp
13
13
 
14
14
  included do
15
15
  after_action :set_test_cookie
16
- rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
16
+ rescue_from ShopifyAPI::Errors::HttpResponseError, with: :handle_http_error
17
17
  end
18
18
 
19
- ACCESS_TOKEN_REQUIRED_HEADER = 'X-Shopify-API-Request-Failure-Unauthorized'
19
+ ACCESS_TOKEN_REQUIRED_HEADER = "X-Shopify-API-Request-Failure-Unauthorized"
20
20
 
21
21
  def activate_shopify_session
22
- if user_session_expected? && user_session.blank?
22
+ if current_shopify_session.blank?
23
23
  signal_access_token_required
24
24
  return redirect_to_login
25
25
  end
26
26
 
27
- return redirect_to_login if current_shopify_session.blank?
27
+ unless current_shopify_session.scope.to_a.empty? ||
28
+ current_shopify_session.scope.covers?(ShopifyAPI::Context.scope)
28
29
 
29
- clear_top_level_oauth_cookie
30
+ clear_shopify_session
31
+ return redirect_to_login
32
+ end
30
33
 
31
34
  begin
32
- ShopifyAPI::Base.activate_session(current_shopify_session)
35
+ ShopifyAPI::Context.activate_session(current_shopify_session)
33
36
  yield
34
37
  ensure
35
- ShopifyAPI::Base.clear_session
38
+ ShopifyAPI::Context.deactivate_session
36
39
  end
37
40
  end
38
41
 
39
42
  def current_shopify_session
40
43
  @current_shopify_session ||= begin
41
- user_session || shop_session
44
+ cookie_name = ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME
45
+ ShopifyAPI::Utils::SessionUtils.load_current_session(
46
+ auth_header: request.headers["HTTP_AUTHORIZATION"],
47
+ cookies: { cookie_name => cookies.encrypted[cookie_name] },
48
+ is_online: user_session_expected?
49
+ )
50
+ rescue ShopifyAPI::Errors::CookieNotFoundError
51
+ nil
52
+ rescue ShopifyAPI::Errors::InvalidJwtTokenError
53
+ nil
42
54
  end
43
55
  end
44
56
 
45
- def user_session
46
- user_session_by_jwt || user_session_by_cookie
47
- end
48
-
49
- def user_session_by_jwt
50
- return unless ShopifyApp.configuration.allow_jwt_authentication
51
- return unless jwt_shopify_user_id
52
- ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
53
- end
54
-
55
- def user_session_by_cookie
56
- return unless ShopifyApp.configuration.allow_cookie_authentication
57
- return unless session[:user_id].present?
58
- ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
59
- end
60
-
61
- def shop_session
62
- shop_session_by_jwt || shop_session_by_cookie
57
+ def login_again_if_different_user_or_shop
58
+ return unless session_id_conflicts_with_params || session_shop_conflicts_with_params
59
+ clear_shopify_session
60
+ redirect_to_login
63
61
  end
64
62
 
65
- def shop_session_by_jwt
66
- return unless ShopifyApp.configuration.allow_jwt_authentication
67
- return unless jwt_shopify_domain
68
- ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
63
+ def signal_access_token_required
64
+ response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
69
65
  end
70
66
 
71
- def shop_session_by_cookie
72
- return unless ShopifyApp.configuration.allow_cookie_authentication
73
- return unless session[:shop_id].present?
74
- ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
67
+ def jwt_expire_at
68
+ expire_at = request.env["jwt.expire_at"]
69
+ return unless expire_at
70
+ expire_at - 5.seconds # 5s gap to start fetching new token in advance
75
71
  end
76
72
 
77
- def login_again_if_different_user_or_shop
78
- if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
79
- clear_session = session[:user_session] != params[:session] # current user is different from stored user
80
- end
73
+ def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
74
+ if request.xhr? && (ignore_response_code || response.code.to_i == 401)
75
+ # Make sure the shop is set in the redirection URL
76
+ unless params[:shop]
77
+ params[:shop] = if current_shopify_session
78
+ current_shopify_session.shop
79
+ elsif (matches = request.headers["HTTP_AUTHORIZATION"]&.match(/^Bearer (.+)$/))
80
+ jwt_payload = ShopifyAPI::Auth::JwtPayload.new(T.must(matches[1]))
81
+ jwt_payload.shop
82
+ end
83
+ end
81
84
 
82
- if current_shopify_session &&
83
- params[:shop] && params[:shop].is_a?(String) &&
84
- (current_shopify_session.domain != params[:shop])
85
- clear_session = true
86
- end
85
+ url ||= login_url_with_optional_shop
87
86
 
88
- if clear_session
89
- clear_shopify_session
90
- redirect_to_login
87
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
88
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
91
89
  end
92
90
  end
93
91
 
94
- def signal_access_token_required
95
- response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
96
- end
97
-
98
92
  protected
99
93
 
100
94
  def jwt_shopify_domain
101
- request.env['jwt.shopify_domain']
95
+ request.env["jwt.shopify_domain"]
102
96
  end
103
97
 
104
98
  def jwt_shopify_user_id
105
- request.env['jwt.shopify_user_id']
99
+ request.env["jwt.shopify_user_id"]
106
100
  end
107
101
 
108
102
  def host
109
- return params[:host] if params[:host].present?
110
-
111
- raise ShopifyHostNotFound
103
+ params[:host]
112
104
  end
113
105
 
114
106
  def redirect_to_login
115
107
  if request.xhr?
108
+ add_top_level_redirection_headers(ignore_response_code: true)
116
109
  head(:unauthorized)
117
110
  else
118
111
  if request.get?
@@ -133,12 +126,16 @@ module ShopifyApp
133
126
  redirect_to(login_url_with_optional_shop)
134
127
  end
135
128
 
129
+ def handle_http_error(error)
130
+ if error.code == 401
131
+ close_session
132
+ else
133
+ raise error
134
+ end
135
+ end
136
+
136
137
  def clear_shopify_session
137
- session[:shop_id] = nil
138
- session[:user_id] = nil
139
- session[:shopify_domain] = nil
140
- session[:shopify_user] = nil
141
- session[:user_session] = nil
138
+ cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = nil
142
139
  end
143
140
 
144
141
  def login_url_with_optional_shop(top_level: false)
@@ -166,28 +163,30 @@ module ShopifyApp
166
163
  query_params[:shop] ||= referer_sanitized_shop_name
167
164
  end
168
165
 
166
+ if params[:host].present?
167
+ query_params[:host] ||= host
168
+ end
169
+
169
170
  query_params[:top_level] = true if top_level
170
171
  query_params
171
172
  end
172
173
 
173
174
  def return_to_param_required?
174
- native_params = %i[shop hmac timestamp locale protocol return_to]
175
- request.path != '/' || sanitized_params.except(*native_params).any?
175
+ native_params = [:shop, :hmac, :timestamp, :locale, :protocol, :return_to]
176
+ request.path != "/" || sanitized_params.except(*native_params).any?
176
177
  end
177
178
 
178
179
  def fullpage_redirect_to(url)
179
180
  if ShopifyApp.configuration.embedded_app?
180
- render('shopify_app/shared/redirect', layout: false,
181
- locals: { url: url, current_shopify_domain: current_shopify_domain })
181
+ render("shopify_app/shared/redirect", layout: false,
182
+ locals: { url: url, current_shopify_domain: current_shopify_domain })
182
183
  else
183
184
  redirect_to(url)
184
185
  end
185
186
  end
186
187
 
187
188
  def current_shopify_domain
188
- shopify_domain = sanitized_shop_name ||
189
- jwt_shopify_domain ||
190
- session[:shopify_domain]
189
+ shopify_domain = sanitized_shop_name || current_shopify_session&.shop
191
190
 
192
191
  return shopify_domain if shopify_domain.present?
193
192
 
@@ -244,7 +243,22 @@ module ShopifyApp
244
243
 
245
244
  private
246
245
 
246
+ def session_id_conflicts_with_params
247
+ shopify_session_id = current_shopify_session&.shopify_session_id
248
+ params[:session].present? && shopify_session_id.present? && params[:session] != shopify_session_id
249
+ end
250
+
251
+ def session_shop_conflicts_with_params
252
+ current_shopify_session && params[:shop].is_a?(String) && current_shopify_session.shop != params[:shop]
253
+ end
254
+
255
+ def shop_session
256
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(sanitize_shop_param(params))
257
+ end
258
+
247
259
  def user_session_expected?
260
+ return false if shop_session.nil?
261
+ return false if ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_session.shop)
248
262
  !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
249
263
  end
250
264
  end