shopify_app 22.0.1 → 22.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +1 -2
  3. data/.rubocop.yml +0 -1
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile.lock +20 -17
  6. data/README.md +38 -0
  7. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +11 -5
  8. data/app/controllers/concerns/shopify_app/ensure_installed.rb +8 -2
  9. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
  10. data/app/controllers/shopify_app/callback_controller.rb +10 -1
  11. data/app/controllers/shopify_app/sessions_controller.rb +24 -4
  12. data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
  13. data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
  14. data/config/routes.rb +1 -0
  15. data/docs/Troubleshooting.md +0 -23
  16. data/docs/Upgrading.md +25 -0
  17. data/docs/shopify_app/authentication.md +105 -20
  18. data/docs/shopify_app/sessions.md +110 -14
  19. data/docs/shopify_app/webhooks.md +1 -1
  20. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +2 -0
  21. data/lib/shopify_app/admin_api/with_token_refetch.rb +28 -0
  22. data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
  23. data/lib/shopify_app/auth/token_exchange.rb +73 -0
  24. data/lib/shopify_app/configuration.rb +54 -3
  25. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +1 -1
  26. data/lib/shopify_app/controller_concerns/embedded_app.rb +27 -0
  27. data/lib/shopify_app/controller_concerns/ensure_billing.rb +11 -3
  28. data/lib/shopify_app/controller_concerns/login_protection.rb +8 -23
  29. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
  30. data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
  31. data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +41 -0
  32. data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
  33. data/lib/shopify_app/session/jwt.rb +9 -0
  34. data/lib/shopify_app/version.rb +1 -1
  35. data/lib/shopify_app.rb +9 -0
  36. data/package.json +1 -1
  37. data/shopify_app.gemspec +1 -1
  38. data/yarn.lock +3 -3
  39. metadata +12 -5
@@ -3,12 +3,11 @@
3
3
  Sessions are used to make contextual API calls for either a shop (offline session) or a user (online session). This gem has ownership of session persistence.
4
4
 
5
5
  #### Table of contents
6
-
7
6
  - [Sessions](#sessions)
8
7
  - [Table of contents](#table-of-contents)
9
8
  - [Sessions](#sessions-1)
10
- - [Types of session tokens](#types-of-session-tokens)
11
- - [Session token storage](#session-token-storage)
9
+ - [Types of access tokens (sessions)](#types-of-access-tokens-sessions)
10
+ - [Access token storage (session)](#access-token-storage-session)
12
11
  - [Shop (offline) token storage](#shop-offline-token-storage)
13
12
  - [User (online) token storage](#user-online-token-storage)
14
13
  - [In-memory Session Storage for testing](#in-memory-session-storage-for-testing)
@@ -20,6 +19,7 @@ Sessions are used to make contextual API calls for either a shop (offline sessio
20
19
  - [**Shop Sessions - `EnsureInstalled`**](#shop-sessions---ensureinstalled)
21
20
  - [User Sessions - `EnsureHasSession`](#user-sessions---ensurehassession)
22
21
  - [Getting sessions from a Shop or User model record - 'with\_shopify\_session'](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session)
22
+ - [Re-fetching an access token when API returns Unauthorized](#re-fetching-an-access-token-when-api-returns-unauthorized)
23
23
  - [Access scopes](#access-scopes)
24
24
  - [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
25
25
  - [`ShopifyApp::UserSessionStorageWithScopes`](#shopifyappusersessionstoragewithscopes)
@@ -27,18 +27,16 @@ Sessions are used to make contextual API calls for either a shop (offline sessio
27
27
  - [Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage)
28
28
 
29
29
  ## Sessions
30
- #### Types of session tokens
31
- - **Shop** ([offline access](https://shopify.dev/docs/apps/auth/oauth/access-modes#offline-access))
30
+ #### Types of access tokens (sessions)
31
+ - **Shop** ([offline access](https://shopify.dev/docs/apps/auth/access-token-types/offline))
32
32
  - Access token is linked to the store
33
33
  - Meant for long-term access to a store, where no user interaction is involved
34
34
  - Ideal for background jobs or maintenance work
35
- - **User** ([online access](https://shopify.dev/docs/apps/auth/oauth/access-modes#online-access))
35
+ - **User** ([online access](https://shopify.dev/docs/apps/auth/access-token-types/online))
36
36
  - Access token is linked to an individual user on a store
37
37
  - Meant to be used when a user is interacting with your app through the web
38
38
 
39
- ⚠️ [Read more about Online vs. Offline access here](https://shopify.dev/apps/auth/oauth/access-modes).
40
-
41
- #### Session token storage
39
+ #### Access token storage (session)
42
40
  ##### Shop (offline) token storage
43
41
  ⚠️ All apps must have a shop session storage, if you started from the [Ruby App Template](https://github.com/Shopify/shopify-app-template-ruby), it's already configured to have a Shop model by default.
44
42
 
@@ -50,7 +48,7 @@ If you don't already have a repository to store the access tokens:
50
48
  rails generate shopify_app:shop_model
51
49
  ```
52
50
 
53
- 2. Configure `config/initializers/shopify_app.rb` to enable shop session token persistance:
51
+ 2. Configure `config/initializers/shopify_app.rb` to enable shop access token persistance:
54
52
 
55
53
  ```ruby
56
54
  config.shop_session_repository = 'Shop'
@@ -66,7 +64,7 @@ If your app has user interactions and would like to control permission based on
66
64
  rails generate shopify_app:user_model
67
65
  ```
68
66
 
69
- 2. Configure `config/initializers/shopify_app.rb` to enable user session token persistance:
67
+ 2. Configure `config/initializers/shopify_app.rb` to enable user access token persistance:
70
68
 
71
69
  ```ruby
72
70
  config.user_session_repository = 'User'
@@ -74,6 +72,8 @@ config.user_session_repository = 'User'
74
72
 
75
73
  The current Shopify user will be stored in the rails session at `session[:shopify_user]`
76
74
 
75
+ You should also enable the [check for session expiry](#expiry-date) so that a new access token can be fetched before being used for an API operation.
76
+
77
77
  ##### In-memory Session Storage for testing
78
78
  The `ShopifyApp` gem includes methods for in-memory storage for both shop and user sessions. In-memory storage is intended to be used in a testing environment, please use a persistent storage for your application.
79
79
  - [InMemoryShopSessionStore](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/in_memory_shop_session_store.rb)
@@ -152,7 +152,7 @@ class MyController < ApplicationController
152
152
 
153
153
  client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
154
154
  client.query(
155
- #...
155
+ # ...
156
156
  )
157
157
  end
158
158
  end
@@ -171,7 +171,7 @@ class MyController < ApplicationController
171
171
 
172
172
  client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_session)
173
173
  client.query(
174
- #...
174
+ # ...
175
175
  )
176
176
  end
177
177
  end
@@ -203,6 +203,100 @@ user.with_shopify_session do
203
203
  end
204
204
  ```
205
205
 
206
+ #### Re-fetching an access token when API returns Unauthorized
207
+
208
+ When using `ShopifyApp::EnsureHasSession` and the `new_embedded_auth_strategy` configuration, any **unhandled** Unauthorized `ShopifyAPI::Errors::HttpResponseError` will cause the app to perform token exchange to fetch a new access token from Shopify and the action to be executed again. This will update and store the new access token to the current session instance.
209
+
210
+ ```ruby
211
+ class MyController < ApplicationController
212
+ include ShopifyApp::EnsureHasSession
213
+
214
+ def index
215
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
216
+
217
+ # If this call raises an Unauthorized error from Shopify, EnsureHasSession
218
+ # will execute the action again after performing token exchange.
219
+ # It will store and use the newly retrieved access token for this and any subsequent calls.
220
+ client.query(options)
221
+ end
222
+ end
223
+ ```
224
+
225
+ If the error is being rescued in the action, it's still possible to make use of `with_token_refetch` provided by `EnsureHasSession` so that a new access token is fetched and the code is executed again with it. This will also update the session parameter with the new attributes.
226
+ This block should be used to wrap the code that makes API queries, so your business logic won't be retried.
227
+
228
+ ```ruby
229
+ class MyController < ApplicationController
230
+ include ShopifyApp::EnsureHasSession
231
+
232
+ def index
233
+ # Your app's business logic
234
+ with_token_refetch(current_shopify_session, shopify_id_token) do
235
+ # Unauthorized errors raised within this block will initiate token exchange.
236
+ # `with_token_refetch` will store the new access token and use it
237
+ # to execute this block again.
238
+ # Any subsequent calls using the same session instance will have the new token.
239
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
240
+ client.query(options)
241
+ end
242
+ # Your app's business logic continues
243
+ rescue => error
244
+ # app's specific error handling
245
+ end
246
+ end
247
+ ```
248
+
249
+ It's also possible to use `with_token_refetch` on classes other than the controller by including the `ShopifyApp::AdminAPI::WithTokenRefetch` module and passing in the session along with the current request's `shopify_id_token`, which is provided by `ShopifyApp::EnsureHasSession`. This will also update the session parameter with the new attributes.
250
+
251
+ ```ruby
252
+ # my_controller.rb
253
+ class MyController < ApplicationController
254
+ include ShopifyApp::EnsureHasSession
255
+
256
+ def index
257
+ # shopify_id_token is a method provided by EnsureHasSession
258
+ MyClass.new.do_things(current_shopify_session, shopify_id_token)
259
+ end
260
+ end
261
+
262
+ # my_class.rb
263
+ class MyClass
264
+ include ShopifyApp::AdminAPI::WithTokenRefetch
265
+
266
+ def do_things(session, shopify_id_token)
267
+ with_token_refetch(session, shopify_id_token) do
268
+ # Unauthorized errors raised within this block will initiate token exchange.
269
+ # `with_token_refetch` will store the new access token and use it
270
+ # to execute this block again.
271
+ # Any subsequent calls using the same session instance will have the new token.
272
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: session)
273
+ client.query(options)
274
+ end
275
+ rescue => error
276
+ # app's specific error handling
277
+ end
278
+ end
279
+ ```
280
+
281
+ If the retried block raises an `Unauthorized` error again, `with_token_refetch` will delete the current `session` from the database and raise the error again.
282
+
283
+ ```ruby
284
+ class MyController < ApplicationController
285
+ include ShopifyApp::EnsureHasSession
286
+
287
+ def index
288
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: current_shopify_session)
289
+ with_token_refetch(current_shopify_session, shopify_id_token) do
290
+ # When this call raises Unauthorized a second time during retry,
291
+ # the `session` will be deleted from the database and the error raised
292
+ client.query(options)
293
+ end
294
+ rescue => error
295
+ # The Unauthorized error will reach this rescue
296
+ end
297
+ end
298
+ ```
299
+
206
300
  ## Access scopes
207
301
  If you want to customize how access scopes are stored for shops and users, you can implement the `access_scopes` getters and setters in the models that include `ShopifyApp::ShopSessionStorageWithScopes` and `ShopifyApp::UserSessionStorageWithScopes` as shown:
208
302
 
@@ -240,6 +334,8 @@ When the configuration flag `check_session_expiry_date` is set to true, the user
240
334
  ## Migrating from shop-based to user-based token strategy
241
335
 
242
336
  1. Run the `user_model` generator as [mentioned above](#user-online-token-storage).
337
+ - The generator will ask whether you want to migrate the User model to include `access_scopes` and `expires_at` columns. `expires_at` field is useful for detecting when the user session has expired and trigger a re-auth before an operation. It can reduce
338
+ API failures for invalid access tokens. Configure the [expiry date check](#expiry-date) to complete this feature.
243
339
  2. Ensure that both your `Shop` model and `User` model includes the [necessary concerns](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods)
244
340
  3. Update the configuration file to use the new session storage.
245
341
 
@@ -255,5 +351,5 @@ config.user_session_repository = {YOUR_USER_MODEL_CLASS}
255
351
  - Sessions storage are now handled with [ShopifyApp::SessionRepository](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/session_repository.rb)
256
352
  - To migrate and specify your shop or user session storage method:
257
353
  1. Remove `session_storage` configuration from `config/initializers/shopify_app.rb`
258
- 2. Follow ["Session Token Storage" instructions](#session-token-storage) to specify the storage repository for shop and user sessions.
354
+ 2. Follow ["Access Token Storage" instructions](#access-token-storage-session) to specify the storage repository for shop and user sessions.
259
355
  - [Customizing session storage](#customizing-session-storage-with-shopifyappsessionrepository)
@@ -18,7 +18,7 @@ ShopifyApp.configure do |config|
18
18
  end
19
19
  ```
20
20
 
21
- When the [OAuth callback](/docs/shopify_app/authentication.md#oauth-callback) is completed successfully, ShopifyApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every OAuth callback, it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.
21
+ When the [OAuth callback](/docs/shopify_app/authentication.md#oauth-callback) or token exchange is completed successfully, ShopifyApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every OAuth callback, it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.
22
22
 
23
23
  ShopifyApp also provides a [WebhooksController](/app/controllers/shopify_app/webhooks_controller.rb) that receives webhooks and queues a job based on the received topic. For example, if you register the webhook from above, then all you need to do is create a job called `CartsUpdateJob`. The job will be queued with 2 params: `shop_domain` and `webhook` (which is the webhook body).
24
24
 
@@ -4,6 +4,8 @@ ShopifyApp.configure do |config|
4
4
  config.scope = "<%= @scope %>" # Consult this page for more scope options:
5
5
  # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
6
6
  config.embedded_app = <%= embedded_app? %>
7
+ config.new_embedded_auth_strategy = <%= embedded_app? %>
8
+
7
9
  config.after_authenticate_job = false
8
10
  config.api_version = "<%= @api_version %>"
9
11
  config.shop_session_repository = 'Shop'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AdminAPI
5
+ module WithTokenRefetch
6
+ def with_token_refetch(session, shopify_id_token)
7
+ retrying = false if retrying.nil?
8
+ yield
9
+ rescue ShopifyAPI::Errors::HttpResponseError => error
10
+ if error.code != 401
11
+ ShopifyApp::Logger.debug("Encountered error: #{error.code} - #{error.response.inspect}, re-raising")
12
+ elsif retrying
13
+ ShopifyApp::Logger.debug("Shopify API returned a 401 Unauthorized error that was not corrected " \
14
+ "with token exchange, deleting current session and re-raising")
15
+ ShopifyApp::SessionRepository.delete_session(session.id)
16
+ else
17
+ retrying = true
18
+ ShopifyApp::Logger.debug("Shopify API returned a 401 Unauthorized error, exchanging token and " \
19
+ "retrying with new session")
20
+ new_session = ShopifyApp::Auth::TokenExchange.perform(shopify_id_token)
21
+ session.copy_attributes_from(new_session)
22
+ retry
23
+ end
24
+ raise
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Auth
5
+ class PostAuthenticateTasks
6
+ class << self
7
+ def perform(session)
8
+ ShopifyApp::Logger.debug("Performing post authenticate tasks")
9
+ # Ensure we use the shop session to install webhooks
10
+ session_for_shop = session.online? ? shop_session(session) : session
11
+
12
+ install_webhooks(session_for_shop)
13
+
14
+ perform_after_authenticate_job(session)
15
+ end
16
+
17
+ private
18
+
19
+ def shop_session(session)
20
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(session.shop)
21
+ end
22
+
23
+ def install_webhooks(session)
24
+ ShopifyApp::Logger.debug("PostAuthenticateTasks: Installing webhooks")
25
+ return unless ShopifyApp.configuration.has_webhooks?
26
+
27
+ WebhooksManager.queue(session.shop, session.access_token)
28
+ end
29
+
30
+ def perform_after_authenticate_job(session)
31
+ ShopifyApp::Logger.debug("PostAuthenticateTasks: Performing after_authenticate_job")
32
+ config = ShopifyApp.configuration.after_authenticate_job
33
+
34
+ return unless config && config[:job].present?
35
+
36
+ job = config[:job]
37
+ job = job.constantize if job.is_a?(String)
38
+
39
+ if config[:inline] == true
40
+ job.perform_now(shop_domain: session.shop)
41
+ else
42
+ job.perform_later(shop_domain: session.shop)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Auth
5
+ class TokenExchange
6
+ attr_reader :id_token
7
+
8
+ def self.perform(id_token)
9
+ new(id_token).perform
10
+ end
11
+
12
+ def initialize(id_token)
13
+ @id_token = id_token
14
+ end
15
+
16
+ def perform
17
+ domain = ShopifyAPI::Auth::JwtPayload.new(id_token).shopify_domain
18
+
19
+ Logger.info("Performing Token Exchange for [#{domain}] - (Offline)")
20
+ session = exchange_token(
21
+ shop: domain,
22
+ id_token: id_token,
23
+ requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
24
+ )
25
+
26
+ if online_token_configured?
27
+ Logger.info("Performing Token Exchange for [#{domain}] - (Online)")
28
+ session = exchange_token(
29
+ shop: domain,
30
+ id_token: id_token,
31
+ requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
32
+ )
33
+ end
34
+
35
+ ShopifyApp.configuration.post_authenticate_tasks.perform(session)
36
+
37
+ session
38
+ end
39
+
40
+ private
41
+
42
+ def exchange_token(shop:, id_token:, requested_token_type:)
43
+ session = ShopifyAPI::Auth::TokenExchange.exchange_token(
44
+ shop: shop,
45
+ session_token: id_token,
46
+ requested_token_type: requested_token_type,
47
+ )
48
+
49
+ SessionRepository.store_session(session)
50
+
51
+ session
52
+ rescue ShopifyAPI::Errors::InvalidJwtTokenError
53
+ Logger.error("Invalid id token '#{id_token}' during token exchange")
54
+ raise
55
+ rescue ShopifyAPI::Errors::HttpResponseError => error
56
+ Logger.error(
57
+ "A #{error.code} error (#{error.class}) occurred during the token exchange. Response: #{error.response.body}",
58
+ )
59
+ raise
60
+ rescue ActiveRecord::RecordNotUnique
61
+ Logger.debug("Session not stored due to concurrent token exchange calls")
62
+ session
63
+ rescue => error
64
+ Logger.error("An error occurred during the token exchange: [#{error.class}] #{error.message}")
65
+ raise
66
+ end
67
+
68
+ def online_token_configured?
69
+ ShopifyApp.configuration.online_token_configured?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -29,6 +29,9 @@ module ShopifyApp
29
29
  attr_writer :login_callback_url
30
30
  attr_accessor :embedded_redirect_url
31
31
 
32
+ # customize post authenticate tasks
33
+ attr_accessor :custom_post_authenticate_tasks
34
+
32
35
  # customise ActiveJob queue names
33
36
  attr_accessor :scripttags_manager_queue_name
34
37
  attr_accessor :webhooks_manager_queue_name
@@ -45,8 +48,8 @@ module ShopifyApp
45
48
  # takes a ShopifyApp::BillingConfiguration object
46
49
  attr_accessor :billing
47
50
 
48
- # Work in Progress: enables token exchange authentication flow
49
- attr_accessor :wip_new_embedded_auth_strategy
51
+ # Enables new authorization flow using token exchange
52
+ attr_accessor :new_embedded_auth_strategy
50
53
 
51
54
  def initialize
52
55
  @root_url = "/"
@@ -54,6 +57,8 @@ module ShopifyApp
54
57
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
55
58
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
56
59
  @disable_webpacker = ENV["SHOPIFY_APP_DISABLE_WEBPACKER"].present?
60
+
61
+ log_callback_controller_method_deprecation
57
62
  end
58
63
 
59
64
  def login_url
@@ -124,7 +129,53 @@ module ShopifyApp
124
129
  end
125
130
 
126
131
  def use_new_embedded_auth_strategy?
127
- wip_new_embedded_auth_strategy && embedded_app?
132
+ new_embedded_auth_strategy && embedded_app?
133
+ end
134
+
135
+ def online_token_configured?
136
+ !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
137
+ end
138
+
139
+ def post_authenticate_tasks
140
+ @post_authenticate_tasks || begin
141
+ if custom_post_authenticate_tasks
142
+ custom_class = if custom_post_authenticate_tasks.respond_to?(:safe_constantize)
143
+ custom_post_authenticate_tasks.safe_constantize
144
+ else
145
+ custom_post_authenticate_tasks
146
+ end
147
+ end
148
+
149
+ task_class = custom_class || ShopifyApp::Auth::PostAuthenticateTasks
150
+
151
+ [
152
+ :perform,
153
+ ].each do |method|
154
+ raise(
155
+ ::ShopifyApp::ConfigurationError,
156
+ "Missing method - '#{method}' for custom_post_authenticate_tasks",
157
+ ) unless task_class.respond_to?(method)
158
+ end
159
+
160
+ task_class
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def log_callback_controller_method_deprecation
167
+ return unless Rails.env.development?
168
+
169
+ # TODO: Remove this before releasing v23.0.0
170
+ message = <<~EOS
171
+ ================================================
172
+ => Upcoming deprecation in v23.0:
173
+ * 'CallbackController::perform_after_authenticate_job' and related methods 'install_webhooks', 'perform_after_authenticate_job'
174
+ * will be deprecated from CallbackController in the next major release. If you need to customize
175
+ * post authentication tasks, see https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/authentication.md#post-authenticate-tasks
176
+ ================================================
177
+ EOS
178
+ puts message
128
179
  end
129
180
  end
130
181
 
@@ -9,7 +9,7 @@ module ShopifyApp
9
9
  end
10
10
 
11
11
  def verify_proxy_request
12
- return head(:forbidden) unless query_string_valid?(request.query_string)
12
+ head(:forbidden) unless query_string_valid?(request.query_string)
13
13
  end
14
14
 
15
15
  private
@@ -5,6 +5,7 @@ module ShopifyApp
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  include ShopifyApp::FrameAncestors
8
+ include ShopifyApp::SanitizedParams
8
9
 
9
10
  included do
10
11
  layout :embedded_app_layout
@@ -13,6 +14,22 @@ module ShopifyApp
13
14
 
14
15
  protected
15
16
 
17
+ def redirect_to_embed_app_in_admin
18
+ ShopifyApp::Logger.debug("Redirecting to embed app in admin")
19
+
20
+ host = if params[:host]
21
+ params[:host]
22
+ elsif params[:shop]
23
+ Base64.encode64("#{sanitized_shop_name}/admin")
24
+ else
25
+ return redirect_to(ShopifyApp.configuration.login_url)
26
+ end
27
+
28
+ redirect_path = ShopifyAPI::Auth.embedded_app_url(host)
29
+ redirect_path = ShopifyApp.configuration.root_url if deduced_phishing_attack?(redirect_path)
30
+ redirect_to(redirect_path, allow_other_host: true)
31
+ end
32
+
16
33
  def use_embedded_app_layout?
17
34
  ShopifyApp.configuration.embedded_app?
18
35
  end
@@ -27,5 +44,15 @@ module ShopifyApp
27
44
  response.set_header("P3P", 'CP="Not used"')
28
45
  response.headers.except!("X-Frame-Options")
29
46
  end
47
+
48
+ def deduced_phishing_attack?(decoded_host)
49
+ sanitized_host = ShopifyApp::Utils.sanitize_shop_domain(decoded_host)
50
+ if sanitized_host.nil?
51
+ message = "Host param for redirect to embed app in admin is not from a trusted domain, " \
52
+ "redirecting to root as this is likely a phishing attack."
53
+ ShopifyApp::Logger.info(message)
54
+ end
55
+ sanitized_host.nil?
56
+ end
30
57
  end
31
58
  end
@@ -27,7 +27,7 @@ module ShopifyApp
27
27
 
28
28
  unless has_payment
29
29
  if request.xhr?
30
- add_top_level_redirection_headers(url: confirmation_url, ignore_response_code: true)
30
+ RedirectForEmbedded.add_app_bridge_redirect_url_header(confirmation_url, response)
31
31
  ShopifyApp::Logger.debug("Responding with 401 unauthorized")
32
32
  head(:unauthorized)
33
33
  elsif ShopifyApp.configuration.embedded_app?
@@ -45,8 +45,16 @@ module ShopifyApp
45
45
  end
46
46
 
47
47
  def handle_billing_error(error)
48
- logger.info("#{error.message}: #{error.errors}")
49
- redirect_to_login
48
+ ShopifyApp::Logger.warn("Encountered billing error - #{error.message}: #{error.errors}\n" \
49
+ "Redirecting to login page")
50
+
51
+ login_url = ShopifyApp.configuration.login_url
52
+ if request.xhr?
53
+ RedirectForEmbedded.add_app_bridge_redirect_url_header(login_url, response)
54
+ head(:unauthorized)
55
+ else
56
+ fullpage_redirect_to(login_url)
57
+ end
50
58
  end
51
59
 
52
60
  def has_active_payment?(session)
@@ -16,6 +16,7 @@ module ShopifyApp
16
16
  end
17
17
 
18
18
  rescue_from ShopifyAPI::Errors::HttpResponseError, with: :handle_http_error
19
+ include ShopifyApp::WithShopifyIdToken
19
20
  end
20
21
 
21
22
  ACCESS_TOKEN_REQUIRED_HEADER = "X-Shopify-API-Request-Failure-Unauthorized"
@@ -53,7 +54,7 @@ module ShopifyApp
53
54
  @current_shopify_session ||= begin
54
55
  cookie_name = ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME
55
56
  load_current_session(
56
- auth_header: request.headers["HTTP_AUTHORIZATION"],
57
+ shopify_id_token: shopify_id_token,
57
58
  cookies: { cookie_name => cookies.encrypted[cookie_name] },
58
59
  is_online: online_token_configured?,
59
60
  )
@@ -78,13 +79,6 @@ module ShopifyApp
78
79
  response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
79
80
  end
80
81
 
81
- def jwt_expire_at
82
- expire_at = request.env["jwt.expire_at"]
83
- return unless expire_at
84
-
85
- expire_at - 5.seconds # 5s gap to start fetching new token in advance
86
- end
87
-
88
82
  def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
89
83
  if request.xhr? && (ignore_response_code || response.code.to_i == 401)
90
84
  ShopifyApp::Logger.debug("Adding top level redirection headers")
@@ -94,8 +88,8 @@ module ShopifyApp
94
88
  params[:shop] = if current_shopify_session
95
89
  current_shopify_session.shop
96
90
 
97
- elsif (matches = request.headers["HTTP_AUTHORIZATION"]&.match(/^Bearer (.+)$/))
98
- jwt_payload = ShopifyAPI::Auth::JwtPayload.new(T.must(matches[1]))
91
+ elsif shopify_id_token
92
+ jwt_payload = ShopifyAPI::Auth::JwtPayload.new(shopify_id_token)
99
93
  jwt_payload.shop
100
94
  end
101
95
  end
@@ -103,21 +97,12 @@ module ShopifyApp
103
97
  url ||= login_url_with_optional_shop
104
98
 
105
99
  ShopifyApp::Logger.debug("Setting Reauthorize-Url to #{url}")
106
- response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
107
- response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
100
+ RedirectForEmbedded.add_app_bridge_redirect_url_header(url, response)
108
101
  end
109
102
  end
110
103
 
111
104
  protected
112
105
 
113
- def jwt_shopify_domain
114
- request.env["jwt.shopify_domain"]
115
- end
116
-
117
- def jwt_shopify_user_id
118
- request.env["jwt.shopify_user_id"]
119
- end
120
-
121
106
  def host
122
107
  params[:host]
123
108
  end
@@ -263,7 +248,7 @@ module ShopifyApp
263
248
  end
264
249
 
265
250
  def online_token_configured?
266
- !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
251
+ ShopifyApp.configuration.online_token_configured?
267
252
  end
268
253
 
269
254
  def user_session_expected?
@@ -273,10 +258,10 @@ module ShopifyApp
273
258
  online_token_configured?
274
259
  end
275
260
 
276
- def load_current_session(auth_header: nil, cookies: nil, is_online: false)
261
+ def load_current_session(shopify_id_token: nil, cookies: nil, is_online: false)
277
262
  return ShopifyAPI::Context.load_private_session if ShopifyAPI::Context.private?
278
263
 
279
- session_id = ShopifyAPI::Utils::SessionUtils.current_session_id(auth_header, cookies, is_online)
264
+ session_id = ShopifyAPI::Utils::SessionUtils.current_session_id(shopify_id_token, cookies, is_online)
280
265
  return nil unless session_id
281
266
 
282
267
  ShopifyApp::SessionRepository.load_session(session_id)
@@ -4,6 +4,11 @@ module ShopifyApp
4
4
  module RedirectForEmbedded
5
5
  include ShopifyApp::SanitizedParams
6
6
 
7
+ def self.add_app_bridge_redirect_url_header(url, response)
8
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
9
+ response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
10
+ end
11
+
7
12
  private
8
13
 
9
14
  def embedded_redirect_url?