Skip to content

Commit 5bad867

Browse files
author
River
committed
Use session token dest claim for shop in token exchange
The exchange_token method now derives the shop from the session token's dest claim (via JwtPayload) rather than using the raw shop parameter. This ensures consistency between the validated token and the shop used for the OAuth request. The shop parameter is still accepted for backward compatibility but the dest claim takes precedence.
1 parent 9deacbb commit 5bad867

2 files changed

Lines changed: 38 additions & 4 deletions

File tree

lib/shopify_api/auth/token_exchange.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ def exchange_token(shop:, session_token:, requested_token_type:)
3636
raise ShopifyAPI::Errors::UnsupportedOauthError,
3737
"Cannot perform OAuth Token Exchange for non embedded apps." unless ShopifyAPI::Context.embedded?
3838

39-
# Validate the session token content
40-
ShopifyAPI::Auth::JwtPayload.new(session_token)
39+
# Validate the session token and use the shop from the token's `dest` claim
40+
jwt_payload = ShopifyAPI::Auth::JwtPayload.new(session_token)
41+
dest_shop = jwt_payload.shop
4142

42-
shop_session = ShopifyAPI::Auth::Session.new(shop: shop)
43+
shop_session = ShopifyAPI::Auth::Session.new(shop: dest_shop)
4344
body = {
4445
client_id: ShopifyAPI::Context.api_key,
4546
client_secret: ShopifyAPI::Context.api_secret_key,
@@ -74,7 +75,7 @@ def exchange_token(shop:, session_token:, requested_token_type:)
7475
session_params = T.cast(response.body, T::Hash[String, T.untyped]).to_h
7576

7677
Session.from(
77-
shop: shop,
78+
shop: dest_shop,
7879
access_token_response: Oauth::AccessTokenResponse.from_hash(session_params),
7980
)
8081
end

test/auth/token_exchange_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,39 @@ def test_exchange_token_offline_token
174174
assert_equal(expected_session, session)
175175
end
176176

177+
def test_exchange_token_uses_shop_from_session_token_dest_claim
178+
modify_context(is_embedded: true, expiring_offline_access_tokens: false)
179+
180+
# Pass a different shop than what's in the JWT dest claim
181+
different_shop = "other-shop.myshopify.com"
182+
183+
# The request should go to the shop from the JWT dest claim, not the passed shop
184+
stub_request(:post, "https://#{@shop}/admin/oauth/access_token")
185+
.with(body: @non_expiring_offline_token_exchange_request)
186+
.to_return(body: @offline_token_response.to_json, headers: { content_type: "application/json" })
187+
188+
expected_session = ShopifyAPI::Auth::Session.new(
189+
id: "offline_#{@shop}",
190+
shop: @shop,
191+
access_token: @offline_token_response[:access_token],
192+
scope: @offline_token_response[:scope],
193+
is_online: false,
194+
expires: nil,
195+
shopify_session_id: @offline_token_response[:session],
196+
refresh_token: nil,
197+
refresh_token_expires: nil,
198+
)
199+
200+
session = ShopifyAPI::Auth::TokenExchange.exchange_token(
201+
shop: different_shop,
202+
session_token: @session_token,
203+
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
204+
)
205+
206+
assert_equal(expected_session, session)
207+
assert_equal(@shop, session.shop)
208+
end
209+
177210
def test_exchange_token_expiring_offline_token
178211
modify_context(is_embedded: true, expiring_offline_access_tokens: true)
179212
stub_request(:post, "https://#{@shop}/admin/oauth/access_token")

0 commit comments

Comments
 (0)