Skip to content

Commit dd158f0

Browse files
Merge branch 'main' into fix/registry-process-with-response-as-struct
2 parents cdd07a9 + eaaa96d commit dd158f0

File tree

4 files changed

+220
-19
lines changed

4 files changed

+220
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
44

55
## Unreleased
66
- [#1312](https://github.com/Shopify/shopify-api-ruby/pull/1312) Use same leeway for `exp` and `nbf` when parsing JWT
7+
- [#1314](https://github.com/Shopify/shopify-api-ruby/pull/1314)
8+
- Add new session util method `SessionUtils::session_id_from_shopify_id_token`
9+
- `SessionUtils::current_session_id` now accepts shopify Id token in the format of `Bearer this_token` or just `this_token`
710

811
## 14.2.0
912
- [#1309](https://github.com/Shopify/shopify-api-ruby/pull/1309) Add `Session#copy_attributes_from` method

docs/getting_started.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,28 @@ Session persistence is handled by the [ShopifyApp](https://github.com/Shopify/sh
4646
#### Cookie
4747
Cookie based authentication is not supported for embedded apps due to browsers dropping support for third party cookies due to security concerns. Non-embedded apps are able to use cookies for session storage/retrieval.
4848

49-
For *non-embedded* apps, you can pass the cookies into `ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, true)` for online (user) sessions or `ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, false)` for offline (store) sessions.
49+
For *non-embedded* apps, you can pass the cookies into:
50+
- `ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, true)` for online (user) sessions or
51+
- `ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, false)` for offline (store) sessions.
5052

5153
#### Getting Session ID From Embedded Requests
52-
For *embedded* apps, you can pass the auth header into `ShopifyAPI::Utils::SessionUtils.current_session_id(auth_header, nil, true)` for online (user) sessions or `ShopifyAPI::Utils::SessionUtils.current_session_id(auth_header, nil, false)` for offline (store) sessions. This function needs an `auth_header` which is the `HTTP_AUTHORIZATION` header.
5354

5455
If your app uses client side rendering instead of server side rendering, you will need to use App Bridge's [authenticatedFetch](https://shopify.dev/docs/apps/auth/oauth/session-tokens/getting-started) to make authenticated API requests from the client.
5556

57+
For *embedded* apps:
58+
59+
If you have an `HTTP_AUTHORIZATION` header or `id_token` from the request URL params , you can pass that as `shopify_id_token` into:
60+
- `ShopifyAPI::Utils::SessionUtils.current_session_id(shopify_id_token, nil, true)` for online (user) sessions or
61+
- `ShopifyAPI::Utils::SessionUtils.current_session_id(shopify_id_token, nil, false)` for offline (store) sessions.
62+
63+
`current_session_id` accepts shopify_id_token in the format of `Bearer this_token` or just `this_token`.
64+
65+
You can also use this method to get session ID:
66+
- `ShopifyAPI::Utils::SessionUtils::session_id_from_shopify_id_token(id_token: id_token, online: true)` for online (user) sessions or
67+
- `ShopifyAPI::Utils::SessionUtils::session_id_from_shopify_id_token(id_token: id_token, online: false)` for offline (store) sessions.
68+
69+
`session_id_from_shopify_id_token` does **NOT** accept shopify_id_token in the format of `Bearer this_token`, you must pass in `this_token`.
70+
5671
#### Start Making Authenticated Shopify Requests
5772

5873
You can now start making authenticated Shopify API calls using the Admin [REST](usage/rest.md) or [GraphQL](usage/graphql.md) Clients or the [Storefront GraphQL Client](usage/graphql_storefront.md).

lib/shopify_api/utils/session_utils.rb

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,16 @@ class << self
1111

1212
sig do
1313
params(
14-
auth_header: T.nilable(String),
14+
shopify_id_token: T.nilable(String),
1515
cookies: T.nilable(T::Hash[String, String]),
1616
online: T::Boolean,
1717
).returns(T.nilable(String))
1818
end
19-
def current_session_id(auth_header, cookies, online)
19+
def current_session_id(shopify_id_token, cookies, online)
2020
if Context.embedded?
21-
if auth_header
22-
matches = auth_header.match(/^Bearer (.+)$/)
23-
unless matches
24-
ShopifyAPI::Logger.warn("Missing Bearer token in authorization header")
25-
raise Errors::MissingJwtTokenError, "Missing Bearer token in authorization header"
26-
end
27-
28-
jwt_payload = Auth::JwtPayload.new(T.must(matches[1]))
29-
shop = jwt_payload.shop
30-
31-
if online
32-
jwt_session_id(shop, jwt_payload.sub)
33-
else
34-
offline_session_id(shop)
35-
end
21+
if shopify_id_token
22+
id_token = shopify_id_token.gsub("Bearer ", "")
23+
session_id_from_shopify_id_token(id_token: id_token, online: online)
3624
else
3725
# falling back to session cookie
3826
raise Errors::CookieNotFoundError, "JWT token or Session cookie not found for app" unless
@@ -48,6 +36,25 @@ def current_session_id(auth_header, cookies, online)
4836
end
4937
end
5038

39+
sig do
40+
params(
41+
id_token: T.nilable(String),
42+
online: T::Boolean,
43+
).returns(String)
44+
end
45+
def session_id_from_shopify_id_token(id_token:, online:)
46+
raise Errors::MissingJwtTokenError, "Missing Shopify ID Token" if id_token.nil? || id_token.empty?
47+
48+
payload = Auth::JwtPayload.new(id_token)
49+
shop = payload.shop
50+
51+
if online
52+
jwt_session_id(shop, payload.sub)
53+
else
54+
offline_session_id(shop)
55+
end
56+
end
57+
5158
sig { params(shop: String, user_id: String).returns(String) }
5259
def jwt_session_id(shop, user_id)
5360
"#{shop}_#{user_id}"

test/utils/session_utils_test.rb

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require_relative "../test_helper"
5+
6+
module ShopifyAPITest
7+
module Utils
8+
class SessionUtils < Test::Unit::TestCase
9+
def setup
10+
super
11+
@user_id = "my_user_id"
12+
@shop = "test-shop.myshopify.io"
13+
14+
@jwt_payload = {
15+
iss: "https://#{@shop}/admin",
16+
dest: "https://#{@shop}",
17+
aud: ShopifyAPI::Context.api_key,
18+
sub: @user_id,
19+
exp: (Time.now + 10).to_i,
20+
nbf: 1234,
21+
iat: 1234,
22+
jti: "4321",
23+
sid: "abc123",
24+
}
25+
26+
@jwt_token = JWT.encode(@jwt_payload, ShopifyAPI::Context.api_secret_key, "HS256")
27+
@auth_header = "Bearer #{@jwt_token}"
28+
@expected_online_session_id = "#{@shop}_#{@user_id}"
29+
@expected_offline_session_id = "offline_#{@shop}"
30+
end
31+
32+
def test_gets_online_session_id_from_shopify_id_token
33+
assert_equal(
34+
@expected_online_session_id,
35+
ShopifyAPI::Utils::SessionUtils.session_id_from_shopify_id_token(id_token: @jwt_token, online: true),
36+
)
37+
end
38+
39+
def test_gets_offline_session_id_from_shopify_id_token
40+
assert_equal(
41+
@expected_offline_session_id,
42+
ShopifyAPI::Utils::SessionUtils.session_id_from_shopify_id_token(id_token: @jwt_token, online: false),
43+
)
44+
end
45+
46+
def test_session_id_from_shopify_id_token_raises_invalid_jwt_errors
47+
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
48+
ShopifyAPI::Utils::SessionUtils.session_id_from_shopify_id_token(id_token: "invalid_token", online: true)
49+
end
50+
end
51+
52+
def test_session_id_from_shopify_id_token_raises_missing_jwt_token_error
53+
[
54+
nil,
55+
"",
56+
].each do |missing_jwt|
57+
error = assert_raises(ShopifyAPI::Errors::MissingJwtTokenError) do
58+
ShopifyAPI::Utils::SessionUtils.session_id_from_shopify_id_token(id_token: missing_jwt, online: true)
59+
end
60+
61+
assert_equal("Missing Shopify ID Token", error.message)
62+
end
63+
end
64+
65+
def test_non_embedded_app_current_session_id_raises_cookie_not_found_error
66+
ShopifyAPI::Context.stubs(:embedded?).returns(false)
67+
68+
[
69+
nil,
70+
{},
71+
{ "not-session-cookie-name": "not-this-cookie" },
72+
].each do |cookies|
73+
error = assert_raises(ShopifyAPI::Errors::CookieNotFoundError) do
74+
ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, true)
75+
end
76+
assert_equal("Session cookie not found for app", error.message)
77+
end
78+
end
79+
80+
def test_non_embedded_app_current_session_id_returns_id_from_cookie
81+
ShopifyAPI::Context.stubs(:embedded?).returns(false)
82+
expected_session_id = "cookie_value"
83+
cookies = { ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME => expected_session_id }
84+
85+
assert_equal(
86+
expected_session_id,
87+
ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, true),
88+
)
89+
end
90+
91+
def test_embedded_app_current_session_id_raises_cookie_not_found_error
92+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
93+
94+
[
95+
nil,
96+
{},
97+
{ "not-session-cookie-name": "not-this-cookie" },
98+
].each do |cookies|
99+
error = assert_raises(ShopifyAPI::Errors::CookieNotFoundError) do
100+
ShopifyAPI::Utils::SessionUtils.current_session_id(nil, cookies, true)
101+
end
102+
assert_equal("JWT token or Session cookie not found for app", error.message)
103+
end
104+
end
105+
106+
def test_embedded_app_current_session_id_raises_invalid_jwt_token_error
107+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
108+
[
109+
"Bearer invalid_token",
110+
"Bearer",
111+
"invalid_token",
112+
].each do |invalid_token|
113+
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError, " - #{invalid_token}") do
114+
ShopifyAPI::Utils::SessionUtils.current_session_id(invalid_token, nil, true)
115+
end
116+
end
117+
end
118+
119+
def test_embedded_app_current_session_id_raises_missing_jwt_token_error
120+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
121+
122+
error = assert_raises(ShopifyAPI::Errors::MissingJwtTokenError) do
123+
ShopifyAPI::Utils::SessionUtils.current_session_id("", nil, true)
124+
end
125+
126+
assert_equal("Missing Shopify ID Token", error.message)
127+
end
128+
129+
def test_embedded_app_current_session_id_returns_online_id_from_auth_header
130+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
131+
132+
assert_equal(
133+
@expected_online_session_id,
134+
ShopifyAPI::Utils::SessionUtils.current_session_id(@auth_header, nil, true),
135+
)
136+
end
137+
138+
def test_embedded_app_current_session_id_returns_offline_id_from_auth_header
139+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
140+
141+
assert_equal(
142+
@expected_offline_session_id,
143+
ShopifyAPI::Utils::SessionUtils.current_session_id(@auth_header, nil, false),
144+
)
145+
end
146+
147+
def test_embedded_app_current_session_id_returns_online_id_from_shopify_id_token
148+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
149+
150+
assert_equal(
151+
@expected_online_session_id,
152+
ShopifyAPI::Utils::SessionUtils.current_session_id(@jwt_token, nil, true),
153+
)
154+
end
155+
156+
def test_embedded_app_current_session_id_returns_offline_id_from_shopify_id_token
157+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
158+
159+
assert_equal(
160+
@expected_offline_session_id,
161+
ShopifyAPI::Utils::SessionUtils.current_session_id(@jwt_token, nil, false),
162+
)
163+
end
164+
165+
def test_embedded_app_current_session_id_returns_id_from_auth_header_even_with_cookies
166+
ShopifyAPI::Context.stubs(:embedded?).returns(true)
167+
cookies = { ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME => "cookie_value" }
168+
169+
assert_equal(
170+
@expected_online_session_id,
171+
ShopifyAPI::Utils::SessionUtils.current_session_id(@auth_header, cookies, true),
172+
)
173+
end
174+
end
175+
end
176+
end

0 commit comments

Comments
 (0)