shopify_app 18.1.1 → 20.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +3 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +39 -3
  5. data/Gemfile +3 -2
  6. data/Gemfile.lock +128 -162
  7. data/README.md +0 -1
  8. data/Rakefile +4 -3
  9. data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +10 -0
  10. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +2 -3
  11. data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +1 -0
  12. data/app/assets/javascripts/shopify_app/redirect.js +6 -8
  13. data/app/controllers/concerns/shopify_app/authenticated.rb +3 -0
  14. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +1 -1
  15. data/app/controllers/concerns/shopify_app/require_known_shop.rb +1 -0
  16. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +1 -1
  17. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  18. data/app/controllers/shopify_app/callback_controller.rb +49 -134
  19. data/app/controllers/shopify_app/sessions_controller.rb +26 -131
  20. data/app/controllers/shopify_app/webhooks_controller.rb +5 -23
  21. data/app/views/shopify_app/sessions/enable_cookies.html.erb +1 -1
  22. data/app/views/shopify_app/sessions/request_storage_access.html.erb +1 -1
  23. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +1 -1
  24. data/app/views/shopify_app/shared/redirect.html.erb +1 -1
  25. data/config/routes.rb +20 -12
  26. data/docs/Releasing.md +1 -1
  27. data/docs/Troubleshooting.md +0 -3
  28. data/docs/Upgrading.md +116 -14
  29. data/docs/shopify_app/webhooks.md +1 -1
  30. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +10 -9
  31. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  32. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +4 -3
  33. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +13 -12
  34. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +9 -1
  35. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +7 -6
  36. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +2 -1
  37. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +1 -1
  38. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +3 -3
  39. data/lib/generators/shopify_app/controllers/controllers_generator.rb +4 -3
  40. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +11 -15
  41. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +2 -2
  42. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +3 -3
  43. data/lib/generators/shopify_app/install/install_generator.rb +25 -74
  44. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -1
  45. data/lib/generators/shopify_app/install/templates/session_store.rb +2 -1
  46. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +33 -5
  47. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +3 -3
  48. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +1 -1
  49. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +4 -4
  50. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -0
  51. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  52. data/lib/generators/shopify_app/routes/routes_generator.rb +6 -5
  53. data/lib/generators/shopify_app/routes/templates/routes.rb +5 -5
  54. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +11 -10
  55. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  56. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  57. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
  58. data/lib/generators/shopify_app/user_model/user_model_generator.rb +11 -10
  59. data/lib/generators/shopify_app/views/views_generator.rb +4 -3
  60. data/lib/shopify_app/access_scopes/shop_strategy.rb +2 -2
  61. data/lib/shopify_app/access_scopes/user_strategy.rb +4 -4
  62. data/lib/shopify_app/configuration.rb +33 -14
  63. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +4 -3
  64. data/lib/shopify_app/controller_concerns/csrf_protection.rb +2 -1
  65. data/lib/shopify_app/controller_concerns/embedded_app.rb +4 -3
  66. data/lib/shopify_app/controller_concerns/ensure_billing.rb +254 -0
  67. data/lib/shopify_app/controller_concerns/itp.rb +3 -3
  68. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  69. data/lib/shopify_app/controller_concerns/login_protection.rb +81 -73
  70. data/lib/shopify_app/controller_concerns/payload_verification.rb +3 -2
  71. data/lib/shopify_app/controller_concerns/webhook_verification.rb +2 -1
  72. data/lib/shopify_app/engine.rb +7 -15
  73. data/lib/shopify_app/jobs/scripttags_manager_job.rb +2 -2
  74. data/lib/shopify_app/jobs/webhooks_manager_job.rb +4 -5
  75. data/lib/shopify_app/managers/scripttags_manager.rb +11 -4
  76. data/lib/shopify_app/managers/webhooks_manager.rb +42 -44
  77. data/lib/shopify_app/middleware/jwt_middleware.rb +5 -4
  78. data/lib/shopify_app/session/in_memory_session_store.rb +1 -0
  79. data/lib/shopify_app/session/in_memory_shop_session_store.rb +2 -1
  80. data/lib/shopify_app/session/in_memory_user_session_store.rb +1 -0
  81. data/lib/shopify_app/session/jwt.rb +9 -8
  82. data/lib/shopify_app/session/null_user_session_store.rb +2 -1
  83. data/lib/shopify_app/session/session_repository.rb +37 -0
  84. data/lib/shopify_app/session/session_storage.rb +4 -6
  85. data/lib/shopify_app/session/shop_session_storage.rb +6 -6
  86. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +7 -8
  87. data/lib/shopify_app/session/user_session_storage.rb +19 -6
  88. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +22 -9
  89. data/lib/shopify_app/test_helpers/all.rb +2 -1
  90. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +4 -3
  91. data/lib/shopify_app/utils.rb +4 -10
  92. data/lib/shopify_app/version.rb +2 -1
  93. data/lib/shopify_app.rb +36 -40
  94. data/package.json +1 -1
  95. data/shopify_app.gemspec +22 -21
  96. data/yarn.lock +9 -9
  97. metadata +50 -53
  98. data/app/assets/javascripts/shopify_app/app_bridge_1.30.0.js +0 -1
  99. data/lib/generators/shopify_app/install/templates/omniauth.rb +0 -4
  100. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +0 -8
  101. data/lib/generators/shopify_app/install/templates/user_agent.rb +0 -6
  102. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +0 -34
  103. 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
  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,82 +13,51 @@ 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
63
- end
64
-
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)
69
- end
70
-
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])
75
- end
76
-
77
57
  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
81
-
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
87
-
88
- if clear_session
89
- clear_shopify_session
90
- redirect_to_login
91
- end
58
+ return unless session_id_conflicts_with_params || session_shop_conflicts_with_params
59
+ clear_shopify_session
60
+ redirect_to_login
92
61
  end
93
62
 
94
63
  def signal_access_token_required
@@ -96,29 +65,47 @@ module ShopifyApp
96
65
  end
97
66
 
98
67
  def jwt_expire_at
99
- expire_at = request.env['jwt.expire_at']
68
+ expire_at = request.env["jwt.expire_at"]
100
69
  return unless expire_at
101
70
  expire_at - 5.seconds # 5s gap to start fetching new token in advance
102
71
  end
103
72
 
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
84
+
85
+ url ||= login_url_with_optional_shop
86
+
87
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
88
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
89
+ end
90
+ end
91
+
104
92
  protected
105
93
 
106
94
  def jwt_shopify_domain
107
- request.env['jwt.shopify_domain']
95
+ request.env["jwt.shopify_domain"]
108
96
  end
109
97
 
110
98
  def jwt_shopify_user_id
111
- request.env['jwt.shopify_user_id']
99
+ request.env["jwt.shopify_user_id"]
112
100
  end
113
101
 
114
102
  def host
115
- return params[:host] if params[:host].present?
116
-
117
- raise ShopifyHostNotFound
103
+ params[:host]
118
104
  end
119
105
 
120
106
  def redirect_to_login
121
107
  if request.xhr?
108
+ add_top_level_redirection_headers(ignore_response_code: true)
122
109
  head(:unauthorized)
123
110
  else
124
111
  if request.get?
@@ -139,12 +126,16 @@ module ShopifyApp
139
126
  redirect_to(login_url_with_optional_shop)
140
127
  end
141
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
+
142
137
  def clear_shopify_session
143
- session[:shop_id] = nil
144
- session[:user_id] = nil
145
- session[:shopify_domain] = nil
146
- session[:shopify_user] = nil
147
- session[:user_session] = nil
138
+ cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = nil
148
139
  end
149
140
 
150
141
  def login_url_with_optional_shop(top_level: false)
@@ -172,28 +163,30 @@ module ShopifyApp
172
163
  query_params[:shop] ||= referer_sanitized_shop_name
173
164
  end
174
165
 
166
+ if params[:host].present?
167
+ query_params[:host] ||= host
168
+ end
169
+
175
170
  query_params[:top_level] = true if top_level
176
171
  query_params
177
172
  end
178
173
 
179
174
  def return_to_param_required?
180
- native_params = %i[shop hmac timestamp locale protocol return_to]
181
- 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?
182
177
  end
183
178
 
184
179
  def fullpage_redirect_to(url)
185
180
  if ShopifyApp.configuration.embedded_app?
186
- render('shopify_app/shared/redirect', layout: false,
187
- 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 })
188
183
  else
189
184
  redirect_to(url)
190
185
  end
191
186
  end
192
187
 
193
188
  def current_shopify_domain
194
- shopify_domain = sanitized_shop_name ||
195
- jwt_shopify_domain ||
196
- session[:shopify_domain]
189
+ shopify_domain = sanitized_shop_name || current_shopify_session&.shop
197
190
 
198
191
  return shopify_domain if shopify_domain.present?
199
192
 
@@ -250,7 +243,22 @@ module ShopifyApp
250
243
 
251
244
  private
252
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
+
253
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)
254
262
  !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
255
263
  end
256
264
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module PayloadVerification
4
5
  extend ActiveSupport::Concern
@@ -6,14 +7,14 @@ module ShopifyApp
6
7
  private
7
8
 
8
9
  def shopify_hmac
9
- request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
10
+ request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"]
10
11
  end
11
12
 
12
13
  def hmac_valid?(data)
13
14
  secrets = [ShopifyApp.configuration.secret, ShopifyApp.configuration.old_secret].reject(&:blank?)
14
15
 
15
16
  secrets.any? do |secret|
16
- digest = OpenSSL::Digest.new('sha256')
17
+ digest = OpenSSL::Digest.new("sha256")
17
18
  ActiveSupport::SecurityUtils.secure_compare(
18
19
  shopify_hmac,
19
20
  Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module WebhookVerification
4
5
  extend ActiveSupport::Concern
@@ -17,7 +18,7 @@ module ShopifyApp
17
18
  end
18
19
 
19
20
  def shop_domain
20
- request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
21
+ request.headers["HTTP_X_SHOPIFY_SHOP_DOMAIN"]
21
22
  end
22
23
  end
23
24
  end
@@ -1,36 +1,28 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module RedactJobParams
4
5
  private
5
6
 
6
7
  def args_info(job)
7
- log_disabled_classes = %w(ShopifyApp::ScripttagsManagerJob ShopifyApp::WebhooksManagerJob)
8
+ log_disabled_classes = ["ShopifyApp::ScripttagsManagerJob", "ShopifyApp::WebhooksManagerJob"]
8
9
  return "" if log_disabled_classes.include?(job.class.name)
9
10
  super
10
11
  end
11
12
  end
12
13
 
13
14
  class Engine < Rails::Engine
14
- engine_name 'shopify_app'
15
+ engine_name "shopify_app"
15
16
  isolate_namespace ShopifyApp
16
17
 
17
18
  initializer "shopify_app.assets.precompile" do |app|
18
- app.config.assets.precompile += %w[
19
- shopify_app/redirect.js
20
- shopify_app/post_redirect.js
21
- shopify_app/top_level.js
22
- shopify_app/enable_cookies.js
23
- shopify_app/request_storage_access.js
24
- storage_access.svg
25
- ]
19
+ app.config.assets.precompile += ["shopify_app/redirect.js", "shopify_app/post_redirect.js",
20
+ "shopify_app/top_level.js", "shopify_app/enable_cookies.js",
21
+ "shopify_app/request_storage_access.js", "storage_access.svg",]
26
22
  end
27
23
 
28
24
  initializer "shopify_app.middleware" do |app|
29
- app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
30
-
31
- if ShopifyApp.configuration.allow_jwt_authentication
32
- app.config.middleware.insert_after(ShopifyApp::SameSiteCookieMiddleware, ShopifyApp::JWTMiddleware)
33
- end
25
+ app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::JWTMiddleware)
34
26
  end
35
27
 
36
28
  initializer "shopify_app.redact_job_params" do