Skip to content

Commit d1f442b

Browse files
authored
Merge branch 'main' into liz/update-tax-shipping-type
2 parents 28f4e0f + 661a8de commit d1f442b

File tree

7 files changed

+125
-25
lines changed

7 files changed

+125
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
77

88
## 14.3.0
99
- [#1312](https://github.com/Shopify/shopify-api-ruby/pull/1312) Use same leeway for `exp` and `nbf` when parsing JWT
10+
- [#1313](https://github.com/Shopify/shopify-api-ruby/pull/1313) Fix: Webhook Registry now working with response_as_struct enabled
1011
- [#1314](https://github.com/Shopify/shopify-api-ruby/pull/1314)
1112
- Add new session util method `SessionUtils::session_id_from_shopify_id_token`
1213
- `SessionUtils::current_session_id` now accepts shopify Id token in the format of `Bearer this_token` or just `this_token`

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,4 @@ DEPENDENCIES
160160
webmock
161161

162162
BUNDLED WITH
163-
2.3.4
163+
2.5.9

docs/usage/oauth.md

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,103 @@ Once the library is set up for your project, you'll be able to use it to start a
55
To do this, you can follow the steps below.
66
For more information on authenticating a Shopify app please see the [Types of Authentication](https://shopify.dev/docs/apps/auth#types-of-authentication) page.
77

8+
#### Table of contents
9+
- [Session Persistence](#session-persistence)
10+
- [Supported types of OAuth Flow](#supported-types-of-oauth)
11+
- [Note about Rails](#note-about-rails)
12+
- [Performing OAuth](#performing-oauth-1)
13+
- [Token Exchange](#token-exchange)
14+
- [Authorization Code Grant Flow](#authorization-code-grant-flow)
15+
- [Using OAuth Session to make authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls)
16+
817
## Session Persistence
918
Session persistence is deprecated from the `ShopifyAPI` library gem since [version 12.3.0](https://github.com/Shopify/shopify-api-ruby/blob/main/CHANGELOG.md#version-1230). The responsibility of session storage typically is fulfilled by the web framework middleware.
1019
This API library's focus is on making requests and facilitate session creation.
1120

1221
⚠️ If you're not using the [ShopifyApp](https://github.com/Shopify/shopify_app) gem, you may use ShopifyAPI to perform OAuth to create sessions, but you must implement your own session storage method to persist the session information to be used in authenticated API calls.
1322

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.
27+
28+
1. [Token Exchange](#token-exchange)
29+
- OAuth flow by exchanging the current user's [session token (shopify id token)](https://shopify.dev/docs/apps/auth/session-tokens) for an
30+
[access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md).
31+
- Recommended and is only available for embedded apps
32+
- Doesn't require redirects, which makes authorization faster and prevents flickering when loading the app
33+
- Access scope changes are handled by [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
34+
2. [Authorization Code Grant Flow](#authorization-code-grant-flow)
35+
- OAuth flow that requires the app to redirect the user to Shopify for installation/authorization of the app to access the shop's data.
36+
- Suitable for non-embedded apps
37+
- Installations, and access scope changes are managed by the app
38+
1439
## Note about Rails
1540
If using in the Rails framework, we highly recommend you use the [shopify_app](https://github.com/Shopify/shopify_app) gem to perform OAuth, you won't have to follow the instructions below to start your own OAuth flow.
1641
- See `ShopifyApp`'s [documentation on session storage](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/sessions.md#sessions)
1742

1843
If you aren't using Rails, you can look at how the `ShopifyApp` gem handles OAuth flow for further examples:
19-
- [Session Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb)
20-
- Triggering and redirecting user to **begin** OAuth flow
21-
- [Callback Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/callback_controller.rb)
22-
- Creating / storing sessions to **complete** the OAuth flow
44+
- Token Exchange Flow
45+
- [Token Exchange](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/auth/token_exchange.rb)
46+
- Completes token exchange flow to get online and offline access tokens
47+
- Authorization Code Grant Flow
48+
- [Session Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb)
49+
- Triggering and redirecting user to **begin** OAuth flow
50+
- [Callback Controller](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/callback_controller.rb)
51+
- Creating / storing sessions to **complete** the OAuth flow
2352

2453
## Performing OAuth
54+
### Token Exchange
2555
#### Steps
56+
1. Enable [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
57+
by configuring your scopes [through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration).
58+
2. [Perform token exchange](#perform-token-exchange) to get an access token.
59+
60+
#### Perform Token Exchange
61+
Use [`ShopifyAPI::Auth::TokenExchange`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/token_exchange.rb) to
62+
exchange a [session token](https://shopify.dev/docs/apps/auth/session-tokens) (Shopify Id Token) for an [access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md).
63+
64+
#### Input
65+
| Parameter | Type | Required? | Default Value | Notes |
66+
| -------------- | ---------------------- | :-------: | :-----------: | ----------------------------------------------------------------------------------------------------------- |
67+
| `shop` | `String` | Yes | - | A Shopify domain name in the form `{exampleshop}.myshopify.com`. |
68+
| `session_token` | `String` | Yes| - | The session token (Shopify Id Token) provided by App Bridge in either the request 'Authorization' header or URL param when the app is loaded in Admin. |
69+
| `requested_token_type` | `TokenExchange::RequestedTokenType` | Yes | - | The type of token requested. Online: `TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN` or offline: `TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN`. |
70+
71+
#### Output
72+
This method returns the new `ShopifyAPI::Auth::Session` object from the token exchange,
73+
your app should store this `Session` object to be used later [when making authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls).
74+
75+
#### Example
76+
```ruby
77+
78+
# `shop` is the shop domain name - "this-is-my-example-shop.myshopify.com"
79+
# `session_token` is the session token provided by App Bridge either in:
80+
# - the request 'Authorization' header as `Bearer this-is-the-session_token`
81+
# - or as a URL param `id_token=this-is-the-session_token`
82+
83+
def authenticate(shop, session_token)
84+
session = ShopifyAPI::Auth::TokenExchange.exchange_token(
85+
shop: shop,
86+
session_token: session_token,
87+
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
88+
# or if you're requesting an online access token:
89+
# requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
90+
)
91+
92+
SessionRepository.store_session(session)
93+
end
94+
95+
```
96+
97+
### Authorization Code Grant Flow
98+
##### Steps
2699
1. [Add a route to start OAuth](#1-add-a-route-to-start-oauth)
27100
2. [Add an Oauth callback route](#2-add-an-oauth-callback-route)
28101
3. [Begin OAuth](#3-begin-oauth)
29102
4. [Handle OAuth Callback](#4-handle-oauth-callback)
30-
5. [Using OAuth Session to make authenticated API calls](#5-using-oauth-session-to-make-authenticated-api-calls)
31103

32-
### 1. Add a route to start OAuth
104+
#### 1. Add a route to start OAuth
33105
Add a route to your app to start the OAuth process.
34106

35107
```ruby
@@ -40,7 +112,7 @@ class ShopifyAuthController < ApplicationController
40112
end
41113
```
42114

43-
### 2. Add an OAuth callback route
115+
#### 2. Add an OAuth callback route
44116
After the app is authenticated with Shopify, the Shopify platform will send a request back to your app using this route
45117
(which you will provide as the `redirect_path` parameter to `begin_auth` method, in [step 3 - Begin OAuth](#3-begin-oauth)).
46118
```ruby
@@ -50,7 +122,7 @@ class ShopifyCallbackController < ApplicationController
50122
end
51123
```
52124

53-
### 3. Begin OAuth
125+
#### 3. Begin OAuth
54126
Use [`ShopifyAPI::Auth::Oauth.begin_auth`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/oauth.rb#L22) method to start OAuth process for your app.
55127

56128
#### Input
@@ -74,7 +146,7 @@ Use [`ShopifyAPI::Auth::Oauth.begin_auth`](https://github.com/Shopify/shopify-ap
74146
|`auth_route`|`String`|URI that will be used for redirecting the user to the Shopify Authentication screen|
75147
|`cookie`|`ShopifyAPI::Auth::Oauth::SessionCookie`|A session cookie to store on the user's browser. |
76148

77-
#### Example
149+
##### Example
78150
Your app should take the returned values from the `begin_auth` method and:
79151

80152
1. Set the cookie in the user's browser. We strongly recommend that you use secure, httpOnly cookies for this to help prevent session hijacking.
@@ -109,19 +181,19 @@ end
109181

110182
⚠️ You can see a concrete example in the `ShopifyApp` gem's [SessionController](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb).
111183

112-
### 4. Handle OAuth Callback
184+
#### 4. Handle OAuth Callback
113185
When the user grants permission to the app in Shopify admin, they'll be redirected back to the app's callback route
114186
(configured in [Step 2 - Add an OAuth callback route](#2-add-an-oauth-callback-route)).
115187

116188
Use [`ShopifyAPI::AuthL::Oauth.validate_auth_callback`](https://github.com/Shopify/shopify-api-ruby/blob/main/lib/shopify_api/auth/oauth.rb#L60) method to finalize the OAuth process.
117189

118-
#### Input
190+
##### Input
119191
| Parameter | Type | Notes |
120192
| ------------ | --------| ----------------------------------------------------------------------------------------------------------- |
121193
| `cookies` | `Hash` | All browser cookies in a hash format with key and value as `String` |
122194
| `auth_query` | `ShopifyAPI::Auth::Oauth::AuthQuery`| An `AuthQuery` containing the authorization request information used to validate the request.|
123195

124-
#### Output
196+
##### Output
125197
This method returns a hash containing the new session and a cookie to be set in the browser in form of:
126198
```ruby
127199
{
@@ -134,12 +206,12 @@ This method returns a hash containing the new session and a cookie to be set in
134206
|`session`|`ShopifyAPI::Auth::Session`|A session object that contains necessary information to identify the session like `shop`, `access_token`, `scope`, etc.|
135207
|`cookie` |`ShopifyAPI::Auth::Oauth::SessionCookie`|A session cookie to store on the user's browser. |
136208

137-
#### Example
209+
##### Example
138210
Your app should call `validate_auth_callback` to construct the `Session` object and cookie that will be used later for authenticated API requests.
139211

140212
1. Call `validate_auth_callback` to construct `Session` and `SessionCookie`.
141213
2. Update browser cookies with the new value for the session.
142-
3. Store the `Session` object to be used later when making authenticated API calls.
214+
3. Store the `Session` object to be used later when [making authenticated API calls](#using-oauth-session-to-make-authenticated-api-calls).
143215
- See [Make a GraphQL API call](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/graphql.md), or
144216
[Make a REST API call](https://github.com/Shopify/shopify-api-ruby/blob/main/docs/usage/rest.md) for examples on how to use the result `Session` object.
145217

@@ -182,8 +254,8 @@ end
182254

183255
⚠️ You can see a concrete example in the `ShopifyApp` gem's [CallbackController](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/callback_controller.rb).
184256

185-
### 5. Using OAuth Session to make authenticated API calls
186-
Once your OAuth flow is complete, and you have stored your `Session` object from [Step 4 - Handle OAuth Callback](#4-handle-oauth-callback), you may use that `Session` object to make authenticated API calls.
257+
## Using OAuth Session to make authenticated API calls
258+
Once your OAuth flow is complete, and you have persisted your `Session` object, you may use that `Session` object to make authenticated API calls.
187259

188260
Example:
189261
```ruby

lib/shopify_api/clients/graphql/client.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ def initialize(session:, base_path:, api_version: nil)
2828
variables: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
2929
headers: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
3030
tries: Integer,
31+
response_as_struct: T.nilable(T::Boolean),
3132
).returns(HttpResponse)
3233
end
33-
def query(query:, variables: nil, headers: nil, tries: 1)
34+
def query(query:, variables: nil, headers: nil, tries: 1, response_as_struct: Context.response_as_struct)
3435
body = { query: query, variables: variables }
3536
@http_client.request(
3637
HttpRequest.new(
@@ -42,7 +43,7 @@ def query(query:, variables: nil, headers: nil, tries: 1)
4243
body_type: "application/json",
4344
tries: tries,
4445
),
45-
response_as_struct: Context.response_as_struct || false,
46+
response_as_struct: response_as_struct || false,
4647
)
4748
end
4849
end

lib/shopify_api/clients/graphql/storefront.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ def initialize(shop, storefront_access_token = nil, private_token: nil, public_t
4545
variables: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
4646
headers: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]),
4747
tries: Integer,
48+
response_as_struct: T.nilable(T::Boolean),
4849
).returns(HttpResponse)
4950
end
50-
def query(query:, variables: nil, headers: {}, tries: 1)
51+
def query(query:, variables: nil, headers: {}, tries: 1, response_as_struct: Context.response_as_struct)
5152
T.must(headers).merge!({ @storefront_auth_header => @storefront_access_token })
52-
super(query: query, variables: variables, headers: headers, tries: tries)
53+
super(query: query, variables: variables, headers: headers, tries: tries,
54+
response_as_struct: response_as_struct)
5355
end
5456
end
5557
end

lib/shopify_api/webhooks/registry.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def unregister(topic:, session:)
138138
}
139139
MUTATION
140140

141-
delete_response = client.query(query: delete_mutation)
141+
delete_response = client.query(query: delete_mutation, response_as_struct: false)
142142
raise Errors::WebhookRegistrationError,
143143
"Failed to delete webhook from Shopify" unless delete_response.ok?
144144
result = T.cast(delete_response.body, T::Hash[String, T.untyped])
@@ -170,7 +170,7 @@ def get_webhook_id(topic:, client:)
170170
}
171171
QUERY
172172

173-
fetch_id_response = client.query(query: fetch_id_query)
173+
fetch_id_response = client.query(query: fetch_id_query, response_as_struct: false)
174174
raise Errors::WebhookRegistrationError,
175175
"Failed to fetch webhook from Shopify" unless fetch_id_response.ok?
176176
body = T.cast(fetch_id_response.body, T::Hash[String, T.untyped])
@@ -216,7 +216,7 @@ def process(request)
216216
).returns(T::Hash[Symbol, T.untyped])
217217
end
218218
def webhook_registration_needed?(client, registration)
219-
check_response = client.query(query: registration.build_check_query)
219+
check_response = client.query(query: registration.build_check_query, response_as_struct: false)
220220
raise Errors::WebhookRegistrationError,
221221
"Failed to check if webhook was already registered" unless check_response.ok?
222222
parsed_check_result = registration.parse_check_result(T.cast(check_response.body, T::Hash[String, T.untyped]))
@@ -233,7 +233,8 @@ def webhook_registration_needed?(client, registration)
233233
).returns(T::Hash[String, T.untyped])
234234
end
235235
def send_register_request(client, registration, webhook_id)
236-
register_response = client.query(query: registration.build_register_query(webhook_id: webhook_id))
236+
register_response = client.query(query: registration.build_register_query(webhook_id: webhook_id),
237+
response_as_struct: false)
237238

238239
raise Errors::WebhookRegistrationError, "Failed to register webhook with Shopify" unless register_response.ok?
239240

test/webhooks/registry_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ def test_process
5959
assert(handler_called)
6060
end
6161

62+
def test_process_with_response_as_struct
63+
modify_context(response_as_struct: true)
64+
65+
handler_called = false
66+
67+
handler = TestHelpers::FakeWebhookHandler.new(
68+
lambda do |topic, shop, body,|
69+
assert_equal(@topic, topic)
70+
assert_equal(@shop, shop)
71+
assert_equal({}, body)
72+
handler_called = true
73+
end,
74+
)
75+
76+
ShopifyAPI::Webhooks::Registry.add_registration(
77+
topic: @topic, path: "path", delivery_method: :http, handler: handler,
78+
)
79+
80+
ShopifyAPI::Webhooks::Registry.process(@webhook_request)
81+
82+
assert(handler_called)
83+
end
84+
6285
def test_process_new_handler
6386
handler_called = false
6487

0 commit comments

Comments
 (0)