Skip to content

Commit b8eee39

Browse files
authored
Merge pull request #1369 from BaggioGiacomo/make-jwt-sid-and-sub-claims-optional
Make sub and sid jwt claims optional
2 parents b2836db + a84c930 commit b8eee39

File tree

4 files changed

+87
-27
lines changed

4 files changed

+87
-27
lines changed

CHANGELOG.md

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

66
- [#1362](https://github.com/Shopify/shopify-api-ruby/pull/1362) Add support for client credentials grant
7+
- [#1369](https://github.com/Shopify/shopify-api-ruby/pull/1369) Make `sub` and `sid` jwt claims optional (Checkout ui extension support)
78

89
## 14.8.0
910

lib/shopify_api/auth/jwt_payload.rb

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ class JwtPayload
1010
JWT_EXPIRATION_LEEWAY = JWT_LEEWAY
1111

1212
sig { returns(String) }
13-
attr_reader :iss, :dest, :aud, :sub, :jti, :sid
13+
attr_reader :iss, :dest, :aud, :jti
1414

1515
sig { returns(Integer) }
1616
attr_reader :exp, :nbf, :iat
1717

18+
sig { returns(T.nilable(String)) }
19+
attr_reader :sub, :sid
20+
1821
alias_method :expire_at, :exp
1922

2023
sig { params(token: String).void }
@@ -30,12 +33,12 @@ def initialize(token)
3033
@iss = T.let(payload_hash["iss"], String)
3134
@dest = T.let(payload_hash["dest"], String)
3235
@aud = T.let(payload_hash["aud"], String)
33-
@sub = T.let(payload_hash["sub"], String)
36+
@sub = T.let(payload_hash["sub"], T.nilable(String))
3437
@exp = T.let(payload_hash["exp"], Integer)
3538
@nbf = T.let(payload_hash["nbf"], Integer)
3639
@iat = T.let(payload_hash["iat"], Integer)
3740
@jti = T.let(payload_hash["jti"], String)
38-
@sid = T.let(payload_hash["sid"], String)
41+
@sid = T.let(payload_hash["sid"], T.nilable(String))
3942

4043
raise ShopifyAPI::Errors::InvalidJwtTokenError,
4144
"Session token had invalid API key" unless @aud == Context.api_key
@@ -47,19 +50,9 @@ def shop
4750
end
4851
alias_method :shopify_domain, :shop
4952

50-
sig { returns(Integer) }
53+
sig { returns(T.nilable(Integer)) }
5154
def shopify_user_id
52-
@sub.to_i
53-
end
54-
55-
# TODO: Remove before releasing v11
56-
sig { params(shop: String).returns(T::Boolean) }
57-
def validate_shop(shop)
58-
Context.logger.warn(
59-
"Deprecation notice: ShopifyAPI::Auth::JwtPayload.validate_shop no longer checks the given shop and always " \
60-
"returns true. It will be removed in v11.",
61-
)
62-
true
55+
@sub.to_i if user_id_sub? && admin_session_token?
6356
end
6457

6558
alias_method :eql?, :==
@@ -86,6 +79,16 @@ def decode_token(token, api_secret_key)
8679
rescue JWT::DecodeError => err
8780
raise ShopifyAPI::Errors::InvalidJwtTokenError, "Error decoding session token: #{err.message}"
8881
end
82+
83+
sig { returns(T::Boolean) }
84+
def admin_session_token?
85+
@iss.end_with?("/admin")
86+
end
87+
88+
sig { returns(T::Boolean) }
89+
def user_id_sub?
90+
@sub&.match?(/\A\d+\z/) || false
91+
end
8992
end
9093
end
9194
end

lib/shopify_api/utils/session_utils.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def session_id_from_shopify_id_token(id_token:, online:)
4949
shop = payload.shop
5050

5151
if online
52-
jwt_session_id(shop, payload.sub)
52+
jwt_session_id(shop, T.must(payload.sub))
5353
else
5454
offline_session_id(shop)
5555
end

test/auth/jwt_payload_test.rb

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Auth
88
class JwtPayloadTest < Test::Unit::TestCase
99
def setup
1010
super
11-
@jwt_payload = {
11+
@admin_jwt_payload = {
1212
iss: "https://test-shop.myshopify.io/admin",
1313
dest: "https://test-shop.myshopify.io",
1414
aud: ShopifyAPI::Context.api_key,
@@ -19,12 +19,23 @@ def setup
1919
jti: "4321",
2020
sid: "abc123",
2121
}
22+
23+
@checkout_ui_extension_jwt_payload = {
24+
iss: "https://test-shop.myshopify.io/checkouts",
25+
dest: "test-shop.myshopify.io",
26+
aud: ShopifyAPI::Context.api_key,
27+
sub: "gid://shopify/Customer/123456789",
28+
exp: (Time.now + 10).to_i,
29+
nbf: 1234,
30+
iat: 1234,
31+
jti: "4321",
32+
}
2233
end
2334

2435
def test_decode_jwt_payload_succeeds_with_valid_token
25-
jwt_token = JWT.encode(@jwt_payload, ShopifyAPI::Context.api_secret_key, "HS256")
36+
jwt_token = JWT.encode(@admin_jwt_payload, ShopifyAPI::Context.api_secret_key, "HS256")
2637
decoded = ShopifyAPI::Auth::JwtPayload.new(jwt_token)
27-
assert_equal(@jwt_payload,
38+
assert_equal(@admin_jwt_payload,
2839
{
2940
iss: decoded.iss,
3041
dest: decoded.dest,
@@ -38,14 +49,14 @@ def test_decode_jwt_payload_succeeds_with_valid_token
3849
})
3950

4051
# Helper methods
41-
assert_equal(decoded.expire_at, @jwt_payload[:exp])
52+
assert_equal(decoded.expire_at, @admin_jwt_payload[:exp])
4253
assert_equal("test-shop.myshopify.io", decoded.shopify_domain)
4354
assert_equal("test-shop.myshopify.io", decoded.shop)
4455
assert_equal(1, decoded.shopify_user_id)
4556
end
4657

4758
def test_decode_jwt_payload_succeeds_with_spin_domain
48-
payload = @jwt_payload.dup
59+
payload = @admin_jwt_payload.dup
4960
payload[:iss] = "https://test-shop.other.spin.dev/admin"
5061
payload[:dest] = "https://test-shop.other.spin.dev"
5162
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
@@ -68,14 +79,14 @@ def test_decode_jwt_payload_succeeds_with_spin_domain
6879
end
6980

7081
def test_decode_jwt_payload_fails_with_wrong_key
71-
jwt_token = JWT.encode(@jwt_payload, "Wrong", "HS256")
82+
jwt_token = JWT.encode(@admin_jwt_payload, "Wrong", "HS256")
7283
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
7384
ShopifyAPI::Auth::JwtPayload.new(jwt_token)
7485
end
7586
end
7687

7788
def test_decode_jwt_payload_fails_with_expired_token
78-
payload = @jwt_payload.dup
89+
payload = @admin_jwt_payload.dup
7990
payload[:exp] = (Time.now - 40).to_i
8091
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
8192
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
@@ -84,7 +95,7 @@ def test_decode_jwt_payload_fails_with_expired_token
8495
end
8596

8697
def test_decode_jwt_payload_fails_if_not_activated_yet
87-
payload = @jwt_payload.dup
98+
payload = @admin_jwt_payload.dup
8899
payload[:nbf] = (Time.now + 12).to_i
89100
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
90101
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
@@ -93,7 +104,7 @@ def test_decode_jwt_payload_fails_if_not_activated_yet
93104
end
94105

95106
def test_decode_jwt_payload_fails_with_invalid_api_key
96-
jwt_token = JWT.encode(@jwt_payload, ShopifyAPI::Context.api_secret_key, "HS256")
107+
jwt_token = JWT.encode(@admin_jwt_payload, ShopifyAPI::Context.api_secret_key, "HS256")
97108

98109
modify_context(api_key: "invalid")
99110

@@ -103,7 +114,7 @@ def test_decode_jwt_payload_fails_with_invalid_api_key
103114
end
104115

105116
def test_decode_jwt_payload_succeeds_with_expiration_in_the_past_within_10s_leeway
106-
payload = @jwt_payload.merge(exp: Time.now.to_i - 8)
117+
payload = @admin_jwt_payload.merge(exp: Time.now.to_i - 8)
107118
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
108119

109120
decoded = ShopifyAPI::Auth::JwtPayload.new(jwt_token)
@@ -122,7 +133,7 @@ def test_decode_jwt_payload_succeeds_with_expiration_in_the_past_within_10s_leew
122133
end
123134

124135
def test_decode_jwt_payload_succeeds_with_not_before_in_the_future_within_10s_leeway
125-
payload = @jwt_payload.merge(nbf: Time.now.to_i + 8)
136+
payload = @admin_jwt_payload.merge(nbf: Time.now.to_i + 8)
126137
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
127138

128139
decoded = ShopifyAPI::Auth::JwtPayload.new(jwt_token)
@@ -139,6 +150,51 @@ def test_decode_jwt_payload_succeeds_with_not_before_in_the_future_within_10s_le
139150
sid: decoded.sid,
140151
})
141152
end
153+
154+
def test_decode_jwt_payload_coming_from_checkout_ui_extension
155+
payload = @checkout_ui_extension_jwt_payload.dup
156+
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
157+
decoded = ShopifyAPI::Auth::JwtPayload.new(jwt_token)
158+
assert_equal(payload,
159+
{
160+
iss: decoded.iss,
161+
dest: decoded.dest,
162+
aud: decoded.aud,
163+
sub: decoded.sub,
164+
exp: decoded.exp,
165+
nbf: decoded.nbf,
166+
iat: decoded.iat,
167+
jti: decoded.jti,
168+
})
169+
170+
assert_equal(decoded.expire_at, @checkout_ui_extension_jwt_payload[:exp])
171+
assert_equal("test-shop.myshopify.io", decoded.shopify_domain)
172+
assert_equal("test-shop.myshopify.io", decoded.shop)
173+
assert_nil(decoded.shopify_user_id)
174+
end
175+
176+
def test_decode_jwt_payload_coming_from_checkout_ui_extension_without_user_logged_in
177+
payload = @checkout_ui_extension_jwt_payload.dup
178+
payload[:sub] = nil
179+
jwt_token = JWT.encode(payload, ShopifyAPI::Context.api_secret_key, "HS256")
180+
decoded = ShopifyAPI::Auth::JwtPayload.new(jwt_token)
181+
assert_equal(payload,
182+
{
183+
iss: decoded.iss,
184+
dest: decoded.dest,
185+
aud: decoded.aud,
186+
sub: decoded.sub,
187+
exp: decoded.exp,
188+
nbf: decoded.nbf,
189+
iat: decoded.iat,
190+
jti: decoded.jti,
191+
})
192+
193+
assert_equal(decoded.expire_at, @checkout_ui_extension_jwt_payload[:exp])
194+
assert_equal("test-shop.myshopify.io", decoded.shopify_domain)
195+
assert_equal("test-shop.myshopify.io", decoded.shop)
196+
assert_nil(decoded.shopify_user_id)
197+
end
142198
end
143199
end
144200
end

0 commit comments

Comments
 (0)