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 +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +3 -3
- data/README.md +38 -0
- data/app/controllers/concerns/shopify_app/ensure_installed.rb +4 -3
- data/app/controllers/shopify_app/sessions_controller.rb +5 -1
- data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
- data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
- data/config/routes.rb +1 -0
- data/docs/Troubleshooting.md +0 -23
- data/docs/Upgrading.md +14 -0
- data/docs/shopify_app/authentication.md +69 -6
- data/docs/shopify_app/sessions.md +110 -14
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +2 -0
- data/lib/shopify_app/admin_api/with_token_refetch.rb +28 -0
- data/lib/shopify_app/auth/token_exchange.rb +73 -0
- data/lib/shopify_app/configuration.rb +3 -3
- data/lib/shopify_app/controller_concerns/embedded_app.rb +27 -0
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +11 -3
- data/lib/shopify_app/controller_concerns/login_protection.rb +7 -22
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
- data/lib/shopify_app/controller_concerns/token_exchange.rb +75 -105
- data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +41 -0
- data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
- data/lib/shopify_app/session/jwt.rb +9 -0
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +5 -0
- data/package.json +1 -1
- data/shopify_app.gemspec +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e3a66e5b90c252c2d65a607911157a50a2d43ac799a70ef61bb5efaeab65013
|
4
|
+
data.tar.gz: 9c145d34b0c9a2f7f587ea2653d660ee7477f90f1348461e52c3dd84c6fc8c6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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
|
-
|
21
|
-
|
22
|
-
|
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>
|
File without changes
|
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"
|
data/docs/Troubleshooting.md
CHANGED
@@ -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
|
14
|
-
* [
|
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
|
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
|
-
|
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
|
-
|
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
|
-
###
|
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
|
11
|
-
- [
|
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
|
31
|
-
- **Shop** ([offline access](https://shopify.dev/docs/apps/auth/
|
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/
|
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
|
-
|
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
|
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
|
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 ["
|
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
|