shopify_app 22.1.0 → 22.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0b7ce0abe557e14db6582b5cefd0bde0837c6e7f380934ddf9754311e8696a5
4
- data.tar.gz: 215ce18139c607e2282f2ecf19cbc365f2fff92012bc4ea5e04cc5d3fe33f4ec
3
+ metadata.gz: 0e3a66e5b90c252c2d65a607911157a50a2d43ac799a70ef61bb5efaeab65013
4
+ data.tar.gz: 9c145d34b0c9a2f7f587ea2653d660ee7477f90f1348461e52c3dd84c6fc8c6a
5
5
  SHA512:
6
- metadata.gz: 3ac07379b157096ad6d8c1817058ff2d49401d82af7309c36963b184d2e65053854d02de70cfbebd164a79fa72147a9bda14f209acbb7aadf971f31da9438a1f
7
- data.tar.gz: ae2126091214107335550b527ec183499dd38e96bbcee3ac6a5585b287403b74a14c7a260d1929c65abbe6060f243ef64ebf62ba8adf8c8de3c7392316241e83
6
+ metadata.gz: ca6a9990ca94f975da2139e2e9ca4d85a9e967959e12cb08c2419d5614001b1245f292bff1916f2dfd45002a421a0a65b337148b917e4b56d6406e344c27e456
7
+ data.tar.gz: 2475faa54148b72d0764e29f101c4f6829f47d049cfcd2b800730a4fa1e6bfe35cf3e26f806366174b9e7a589e7a09143b0a4f9ef20cdbceb2e007b357bc63f3
data/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
1
  Unreleased
2
2
  ----------
3
3
 
4
+ 22.2.0 (May 2,2024)
5
+ ----------
6
+ * Add new zero redirect authorization strategy - `Token Exchange`.
7
+ - This strategy replaces the existing OAuth flow for embedded apps and remove the redirects that were previously necessary to complete OAuth.
8
+ See ["New embedded app authorization strategy"](/README.md/#new-embedded-app-authorization-strategy) for how to enable this feature.
9
+ - Related PRs: [#1817](https://github.com/Shopify/shopify_app/pull/1817),
10
+ [#1818](https://github.com/Shopify/shopify_app/pull/1818),
11
+ [#1819](https://github.com/Shopify/shopify_app/pull/1819),
12
+ [#1821](https://github.com/Shopify/shopify_app/pull/1821),
13
+ [#1822](https://github.com/Shopify/shopify_app/pull/1822),
14
+ [#1823](https://github.com/Shopify/shopify_app/pull/1823),
15
+ [#1832](https://github.com/Shopify/shopify_app/pull/1832),
16
+ [#1833](https://github.com/Shopify/shopify_app/pull/1833),
17
+ [#1834](https://github.com/Shopify/shopify_app/pull/1834),
18
+ [#1836](https://github.com/Shopify/shopify_app/pull/1836),
19
+ * Bumps `shopify_api` to `14.3.0` [1832](https://github.com/Shopify/shopify_app/pull/1832)
20
+ * Support `id_token` from URL param [1832](https://github.com/Shopify/shopify_app/pull/1832)
21
+ * Extracted controller concern `WithShopifyIdToken`
22
+ * This concern provides a method `shopify_id_token` to retrieve the Shopify Id token from either the authorization header or the URL param `id_token`.
23
+ * `ShopifyApp::JWTMiddleware` supports retrieving session token from URL param `id_token`
24
+ * `ShopifyApp::JWTMiddleware` returns early if the app is not embedded to avoid unnecessary JWT verification
25
+ * `LoginProtection` now uses `WithShopifyIdToken` concern to retrieve the Shopify Id token, thus accepting the session token from the URL param `id_token`
26
+ * Marking `ShopifyApp::JWT` to be deprecated in version 23.0.0 [1832](https://github.com/Shopify/shopify_app/pull/1832), use `ShopifyAPI::Auth::JwtPayload` instead.
27
+ * Fix infinite redirect loop caused by handling errors from Billing API [1833](https://github.com/Shopify/shopify_app/pull/1833)
28
+
4
29
  22.1.0 (April 9,2024)
5
30
  ----------
6
31
  * Extracted class - `PostAuthenticateTasks` to handle post authenticate tasks. To learn more, see [post authenticate tasks](/docs/shopify_app/authentication.md#post-authenticate-tasks). [1819](https://github.com/Shopify/shopify_app/pull/1819)
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shopify_app (22.1.0)
4
+ shopify_app (22.2.0)
5
5
  activeresource
6
6
  addressable (~> 2.7)
7
7
  jwt (>= 2.2.3)
8
8
  rails (> 5.2.1)
9
9
  redirect_safely (~> 1.0)
10
- shopify_api (>= 14.1.0, < 15.0)
10
+ shopify_api (>= 14.3.0, < 15.0)
11
11
  sprockets-rails (>= 2.0.0)
12
12
 
13
13
  GEM
@@ -217,7 +217,7 @@ GEM
217
217
  ruby-progressbar (1.13.0)
218
218
  ruby2_keywords (0.0.5)
219
219
  securerandom (0.2.2)
220
- shopify_api (14.1.0)
220
+ shopify_api (14.3.0)
221
221
  activesupport
222
222
  concurrent-ruby
223
223
  hash_diff
data/README.md CHANGED
@@ -129,6 +129,44 @@ These routes are configurable. See the more detailed [*Engine*](/docs/shopify_ap
129
129
 
130
130
  To learn more about how this gem authenticates with Shopify, see [*Authentication*](/docs/shopify_app/authentication.md).
131
131
 
132
+ ### New embedded app authorization strategy (Token Exchange)
133
+
134
+ > [!TIP]
135
+ > If you are building an embedded app, we **strongly** recommend using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
136
+ > with [token exchange](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange) instead of the legacy authorization code grant flow.
137
+
138
+ We've introduced a new installation and authorization strategy for **embedded apps** that
139
+ eliminates the redirects that were previously necessary.
140
+ It replaces the existing [installation and authorization code grant flow](https://shopify.dev/docs/apps/auth/get-access-tokens/authorization-code-grant).
141
+
142
+ This is achieved by using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
143
+ to handle automatic app installations and scope updates, while utilizing
144
+ [token exchange](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange) to retrieve an access token for
145
+ authenticated API access.
146
+
147
+ ##### Enabling this new strategy in your app
148
+
149
+ 1. Enable [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
150
+ by configuring your scopes [through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration).
151
+ 2. Enable the new auth strategy in your app's ShopifyApp configuration file.
152
+
153
+ ```ruby
154
+ # config/initializers/shopify_app.rb
155
+ ShopifyApp.configure do |config|
156
+ #.....
157
+ config.embedded_app = true
158
+ config.new_embedded_auth_strategy = true
159
+
160
+ # If your app is configured to use online sessions, you can enable session expiry date check so a new access token
161
+ # is fetched automatically when the session expires.
162
+ # See expiry date check docs: https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md#expiry-date
163
+ config.check_session_expiry_date = true
164
+ ...
165
+ end
166
+
167
+ ```
168
+ 3. Enjoy a smoother and faster app installation process.
169
+
132
170
  ### API Versioning
133
171
 
134
172
  [Shopify's API is versioned](https://shopify.dev/concepts/about-apis/versioning). With Shopify App `v1.11.0`, the included Shopify API gem allows developers to specify and update the Shopify API version they want their app or service to use. The Shopify API gem also surfaces warnings to Rails apps about [deprecated endpoints, GraphQL fields and more](https://shopify.dev/concepts/about-apis/versioning#deprecation-practices).
@@ -17,9 +17,10 @@ module ShopifyApp
17
17
 
18
18
  before_action :check_shop_domain
19
19
 
20
- unless ShopifyApp.configuration.use_new_embedded_auth_strategy?
21
- # TODO: Add support to use new embedded auth strategy here when invalid
22
- # session token can be handled by AppBridge app reload
20
+ if ShopifyApp.configuration.use_new_embedded_auth_strategy?
21
+ include ShopifyApp::TokenExchange
22
+ around_action :activate_shopify_session
23
+ else
23
24
  before_action :check_shop_known
24
25
  before_action :validate_non_embedded_session
25
26
  end
@@ -7,7 +7,7 @@ module ShopifyApp
7
7
 
8
8
  layout false, only: :new
9
9
 
10
- after_action only: [:new, :create] do |controller|
10
+ after_action only: [:new, :create, :patch_shopify_id_token] do |controller|
11
11
  controller.response.headers.except!("X-Frame-Options")
12
12
  end
13
13
 
@@ -19,6 +19,10 @@ module ShopifyApp
19
19
  authenticate
20
20
  end
21
21
 
22
+ def patch_shopify_id_token
23
+ render(layout: "shopify_app/layouts/app_bridge")
24
+ end
25
+
22
26
  def top_level_interaction
23
27
  @url = login_url_with_optional_shop(top_level: true)
24
28
  validate_shop_presence
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title><%= ShopifyApp.configuration.application_name %></title>
6
+ <%= yield :head %>
7
+ <script
8
+ data-api-key="<%= ShopifyApp.configuration.api_key %>"
9
+ src="https://cdn.shopify.com/shopifycloud/app-bridge.js">
10
+ </script>
11
+ <%= csrf_meta_tags %>
12
+ </head>
13
+
14
+ <body>
15
+ <%= yield %>
16
+ </body>
17
+ </html>
data/config/routes.rb CHANGED
@@ -8,6 +8,7 @@ ShopifyApp::Engine.routes.draw do
8
8
  get login_url => :new, :as => :login
9
9
  post login_url => :create, :as => :authenticate
10
10
  get "logout" => :destroy, :as => :logout
11
+ get "patch_shopify_id_token" => :patch_shopify_id_token
11
12
 
12
13
  # Kept to prevent apps relying on these routes from breaking
13
14
  if login_url.gsub(%r{^/}, "") != "login"
@@ -92,29 +92,6 @@ Edit `config/initializer/shopify_app.rb` and ensure the following configurations
92
92
  + config.shop_session_repository = 'Shop'
93
93
  ```
94
94
 
95
- #### Inspect server logs
96
-
97
- If you have checked the configurations above, and the app is still using cookies, then it is possible that the `shopify_app` gem defaulted to relying on cookies. This would happen when your browser allows third-party cookies and a session token was not successfully found as part of your request.
98
-
99
- In this case, check the server logs to see if the session token was invalid:
100
-
101
- ```los
102
- [ShopifyApp::JWT] Failed to validate JWT: [JWT::<Error>] <Failure message>
103
- ```
104
-
105
- *Example*
106
-
107
- ```
108
- [ShopifyApp::JWT] Failed to validate JWT: [JWT::ImmatureSignature] Signature nbf has not been reached
109
- ```
110
-
111
- **Note:** In a local development environment, you may want to temporarily update your `Gemfile` to point to a local instance of the `shopify_app` library instad of an installed gem. This will enable you to use a debugging tool like `byebug` to debug the library.
112
-
113
- ```diff
114
- - gem 'shopify_app', '~> 14.2'
115
- + gem 'shopify_app', path: '/path/to/shopify_app'
116
- ```
117
-
118
95
  ### My app can't make requests to the Shopify API
119
96
 
120
97
  > **Note:** Session tokens cannot be used to make authenticated requests to the Shopify API. Learn more about authenticating your backend requests to Shopify APIs at [Shopify API authentication](https://shopify.dev/concepts/about-apis/authentication).
data/docs/Upgrading.md CHANGED
@@ -8,6 +8,8 @@ This file documents important changes needed to upgrade your app's Shopify App v
8
8
 
9
9
  [Unreleased](#unreleased)
10
10
 
11
+ [Upgrading to `v22.2.0`](#upgrading-to-v2220)
12
+
11
13
  [Upgrading to `v22.0.0`](#upgrading-to-v2200)
12
14
 
13
15
  [Upgrading to `v20.3.0`](#upgrading-to-v2030)
@@ -51,6 +53,18 @@ If you have overwritten these methods in your callback controller to modify the
51
53
  update your app to use configurable option `config.custom_post_authenticate_tasks` instead. See [post authenticate tasks](/docs/shopify_app/authentication.md#post-authenticate-tasks)
52
54
  for more information.
53
55
 
56
+ #### (v23.0.0) - Deprecated "ShopifyApp::JWT" class
57
+ The `ShopifyApp::JWT` class has been deprecated in `v23.0.0`. Use [ShopifyAPI::Auth::JwtPayload](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/jwt_payload.rb)
58
+ class from the `shopify_api` gem instead. A search and replace should be enough for this migration.
59
+ - `ShopifyAPI::Auth::JwtPayload` is a superset of the `ShopifyApp::JWT` class, and contains methods that were available in `ShopifyApp::JWT`.
60
+ - `ShopifyAPI::Auth::JwtPayload` raises `ShopifyAPI::Errors::InvalidJwtTokenError` if the token is invalid.
61
+
62
+ ## Upgrading to `v22.2.0`
63
+ #### Added new feature for zero redirect embedded app authorization flow - Token Exchange
64
+ A new embedded app authorization strategy has been introduced in `v22.2.0` that eliminates the redirects that were previously necessary for OAuth.
65
+ It can replace the existing installation and authorization code grant flow.
66
+ See [new embedded app authorization strategy](./README.md#new-embedded-app-authorization-strategy) for more information.
67
+
54
68
  ## Upgrading to `v22.0.0`
55
69
  #### Dropped support for Ruby 2.x
56
70
  Support for Ruby 2.x has been dropped as it is no longer supported. You'll need to upgrade to 3.x.x
@@ -10,17 +10,73 @@ See [*Getting started with session token authentication*](https://shopify.dev/do
10
10
 
11
11
  #### Table of contents
12
12
 
13
- * [OAuth callback](#oauth-callback)
14
- * [Customizing callback controller](#customizing-callback-controller)
13
+ * [Supported types of OAuth Flow](#supported-types-of-oauth)
14
+ * [Token Exchange](#token-exchange)
15
+ * [Authorization Code Grant Flow](#authorization-code-grant-flow)
16
+ * [OAuth callback](#oauth-callback)
17
+ * [Customizing callback controller](#customizing-callback-controller)
18
+ * [Detecting scope changes](#detecting-scope-changes-1)
15
19
  * [Run jobs after the OAuth flow](#post-authenticate-tasks)
16
20
  * [Rotate API credentials](#rotate-api-credentials)
17
21
  * [Making authenticated API requests after authorization](#making-authenticated-api-requests-after-authorization)
18
22
 
19
- ## OAuth callback
23
+ ## Supported types of OAuth
24
+ > [!TIP]
25
+ > If you are building an embedded app, we **strongly** recommend using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
26
+ with [token exchange](#token-exchange) instead of the authorization code grant flow.
20
27
 
21
- >️ **Note:** In Shopify App version 8.4.0, we have extracted the callback logic in its own controller. If you are upgrading from a version older than 8.4.0 the callback action and related helper methods were defined in `ShopifyApp::SessionsController` ==> you will have to extend `ShopifyApp::CallbackController` instead and port your logic to the new controller.
28
+ 1. [Token Exchange](#token-exchange)
29
+ - Recommended and is only available for embedded apps
30
+ - Doesn't require redirects, which makes authorization faster and prevents flickering when loading the app
31
+ - Access scope changes are handled by Shopify when you use [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
32
+ 2. [Authorization Code Grant Flow](#authorization-code-grant-flow)
33
+ - Suitable for non-embedded apps
34
+ - Installations, and access scope changes are managed by the app
22
35
 
23
- Upon completing the OAuth flow, Shopify calls the app at `ShopifyApp.configuration.login_callback_url`.
36
+ ## Token Exchange
37
+
38
+ OAuth process by exchanging the current user's [session token (shopify id token)](https://shopify.dev/docs/apps/auth/session-tokens) for an
39
+ [access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md) to make
40
+ authenticated Shopify API queries. This will replace authorization code grant flow completely when your app is configured with [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation).
41
+
42
+ To enable token exchange authorization strategy, you can follow the steps in ["New embedded app authorization strategy"](/README.md#new-embedded-app-authorization-strategy).
43
+ Upon completion of the token exchange to get the access token, [post authenticated tasks](#post-authenticate-tasks) will be run.
44
+
45
+ Learn more about:
46
+ - [How token exchange works](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange)
47
+ - [Using Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
48
+ - [Configuring access scopes through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration)
49
+
50
+ #### Handling invalid access tokens
51
+ If the access token used to make an API call is invalid, the token exchange strategy will handle the error and try to retrieve a new access token before retrying
52
+ the same operation.
53
+ See ["Re-fetching an access token when API returns Unauthorized"](/docs/shopify_app/sessions.md#re-fetching-an-access-token-when-api-returns-unauthorized) section for more information.
54
+
55
+ #### Detecting scope changes
56
+
57
+ ##### Shopify managed installation
58
+ If your access scopes are [configured through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration), scope changes will be handled by Shopify automatically.
59
+ Learn more about [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation).
60
+ Using token exchange will ensure that the access token retrieved will always have the latest access scopes granted by the user.
61
+
62
+ ## Authorization Code Grant Flow
63
+ Authorization code grant flow is the OAuth flow that requires the app to redirect the user
64
+ to Shopify for installation/authorization of the app to access the shop's data. It is still required for apps that are not embedded.
65
+
66
+ To perform [authorization code grant flow](https://shopify.dev/docs/apps/auth/get-access-tokens/authorization-code-grant), you app will need to handle
67
+ [begin OAuth](#begin-oauth) and [OAuth callback](#oauth-callback) routes.
68
+
69
+ ### Begin OAuth
70
+ ShopifyApp automatically redirects the user to Shopify to complete OAuth to install the app when the `ShopifyApp.configuration.login_url` is reached.
71
+ Behind the scenes the ShopifyApp gem starts the process by calling `ShopifyAPI::Auth::Oauth.begin_auth` to build the
72
+ redirect URL with necessary parameters like the OAuth callback URL, scopes requested, type of access token (offline or online) requested, etc.
73
+ The ShopifyApp gem then redirect the merchant to Shopify, to ask for permission to install the app. (See [ShopifyApp::SessionsController.redirect_to_begin_oauth](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb#L76-L96)
74
+ for detailed implementation)
75
+
76
+ ### OAuth callback
77
+
78
+ Shopify will redirect the merchant back to your app's callback URL once they approve the app installation.
79
+ Upon completing the OAuth flow, Shopify calls the app at `ShopifyApp.configuration.login_callback_url`. (This was provided to Shopify in the OAuth begin URL parameters)
24
80
 
25
81
  The default callback controller [`ShopifyApp::CallbackController`](../../app/controllers/shopify_app/callback_controller.rb) provides the following behaviour:
26
82
 
@@ -63,7 +119,14 @@ Rails.application.routes.draw do
63
119
  end
64
120
  ```
65
121
 
66
- ### Post Authenticate tasks
122
+ ### Detecting scope changes
123
+ When the OAuth process is completed, the created session has a `scope` field which holds all of the access scopes that were requested from the merchant at the time.
124
+
125
+ When an app's access scopes change, it needs to request merchants to go through OAuth again to renew its permissions.
126
+
127
+ See [Handling changes in access scopes](/docs/shopify_app/handling-access-scopes-changes.md).
128
+
129
+ ## Post Authenticate tasks
67
130
  After authentication is complete, a few tasks are run by default by PostAuthenticateTasks:
68
131
  1. [Installing Webhooks](/docs/shopify_app/webhooks.md)
69
132
  2. [Run configured after_authenticate_job](#after_authenticate_job)
@@ -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)
@@ -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,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