Skip to content

Commit eccef0c

Browse files
authored
Merge pull request #979 from Shopify/add-old-api-secret-key-to-context
Add ShopifyAPI::Context.old_api_secret_key to support API key rotation
2 parents ae6db8a + 72c65af commit eccef0c

File tree

6 files changed

+67
-13
lines changed

6 files changed

+67
-13
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api
44

55
## Unreleased
66

7+
- [#979](https://github.com/Shopify/shopify_api/pull/979) Update `ShopifyAPI::Context.setup` to take `old_api_secret_key` to support API credentials rotation
8+
79
## Version 10.1.0
810

911
- [#933](https://github.com/Shopify/shopify_api/pull/933) Fix syntax of GraphQL query in `Webhooks.get_webhook_id` method by removing extra curly brace

lib/shopify_api/auth/jwt_payload.rb

+16-5
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ class JwtPayload
1616

1717
sig { params(token: String).void }
1818
def initialize(token)
19-
begin
20-
payload_hash = JWT.decode(token, Context.api_secret_key, true,
21-
{ exp_leeway: JWT_EXPIRATION_LEEWAY, algorithm: "HS256" })[0]
22-
rescue
23-
raise ShopifyAPI::Errors::InvalidJwtTokenError, "Failed to parse session token '#{token}'"
19+
payload_hash = begin
20+
decode_token(token, Context.api_secret_key)
21+
rescue ShopifyAPI::Errors::InvalidJwtTokenError
22+
raise unless Context.old_api_secret_key
23+
24+
decode_token(token, T.must(Context.old_api_secret_key))
2425
end
2526

2627
@iss = T.let(payload_hash["iss"], String)
@@ -67,6 +68,16 @@ def ==(other)
6768
jti == other.jti &&
6869
sid == other.sid
6970
end
71+
72+
private
73+
74+
sig { params(token: String, api_secret_key: String).returns(T::Hash[String, T.untyped]) }
75+
def decode_token(token, api_secret_key)
76+
JWT.decode(token, api_secret_key, true,
77+
{ exp_leeway: JWT_EXPIRATION_LEEWAY, algorithm: "HS256" })[0]
78+
rescue
79+
raise ShopifyAPI::Errors::InvalidJwtTokenError, "Failed to parse session token '#{token}'"
80+
end
7081
end
7182
end
7283
end

lib/shopify_api/context.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Context
1818
@notified_missing_resources_folder = T.let({}, T::Hash[String, T::Boolean])
1919
@active_session = T.let(Concurrent::ThreadLocalVar.new { nil }, Concurrent::ThreadLocalVar)
2020
@user_agent_prefix = T.let(nil, T.nilable(String))
21+
@old_api_secret_key = T.let(nil, T.nilable(String))
2122

2223
@rest_resource_loader = T.let(nil, T.nilable(Zeitwerk::Loader))
2324

@@ -37,6 +38,7 @@ class << self
3738
logger: Logger,
3839
private_shop: T.nilable(String),
3940
user_agent_prefix: T.nilable(String),
41+
old_api_secret_key: T.nilable(String),
4042
).void
4143
end
4244
def setup(
@@ -50,7 +52,8 @@ def setup(
5052
session_storage:,
5153
logger: Logger.new($stdout),
5254
private_shop: nil,
53-
user_agent_prefix: nil
55+
user_agent_prefix: nil,
56+
old_api_secret_key: nil
5457
)
5558
unless ShopifyAPI::AdminVersions::SUPPORTED_ADMIN_VERSIONS.include?(api_version)
5659
raise Errors::UnsupportedVersionError,
@@ -68,6 +71,7 @@ def setup(
6871
@logger = logger
6972
@private_shop = private_shop
7073
@user_agent_prefix = user_agent_prefix
74+
@old_api_secret_key = old_api_secret_key
7175

7276
load_rest_resources(api_version: api_version)
7377
end
@@ -118,7 +122,7 @@ def private?
118122
end
119123

120124
sig { returns(T.nilable(String)) }
121-
attr_reader :private_shop, :user_agent_prefix
125+
attr_reader :private_shop, :user_agent_prefix, :old_api_secret_key
122126

123127
sig { returns(T::Boolean) }
124128
def embedded?

test/context_test.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def setup
1818
session_storage: ShopifyAPI::Auth::FileSessionStorage.new,
1919
logger: Logger.new(writer),
2020
private_shop: "privateshop.myshopify.com",
21-
user_agent_prefix: "user_agent_prefix1"
21+
user_agent_prefix: "user_agent_prefix1",
22+
old_api_secret_key: "old_secret"
2223
)
2324
end
2425

@@ -40,6 +41,7 @@ def test_setup
4041
assert_match(/test log/, @reader.gets)
4142
assert_equal("privateshop.myshopify.com", ShopifyAPI::Context.private_shop)
4243
assert_equal("user_agent_prefix1", ShopifyAPI::Context.user_agent_prefix)
44+
assert_equal("old_secret", ShopifyAPI::Context.old_api_secret_key)
4345
end
4446

4547
def test_active_session_is_thread_safe
@@ -125,7 +127,8 @@ def clear_context
125127
is_private: false,
126128
is_embedded: true,
127129
session_storage: ShopifyAPI::Auth::FileSessionStorage.new,
128-
user_agent_prefix: nil
130+
user_agent_prefix: nil,
131+
old_api_secret_key: nil
129132
)
130133
end
131134
end

test/test_helper.rb

+8-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def setup
3131
is_private: false,
3232
is_embedded: false,
3333
session_storage: TestHelpers::FakeSessionStorage.new,
34-
user_agent_prefix: nil
34+
user_agent_prefix: nil,
35+
old_api_secret_key: nil
3536
)
3637
end
3738

@@ -47,7 +48,8 @@ def setup
4748
session_storage: T.nilable(ShopifyAPI::Auth::SessionStorage),
4849
logger: T.nilable(Logger),
4950
private_shop: T.nilable(String),
50-
user_agent_prefix: T.nilable(String)
51+
user_agent_prefix: T.nilable(String),
52+
old_api_secret_key: T.nilable(String)
5153
).void
5254
end
5355
def modify_context(
@@ -61,7 +63,8 @@ def modify_context(
6163
session_storage: nil,
6264
logger: nil,
6365
private_shop: "do-not-set",
64-
user_agent_prefix: nil
66+
user_agent_prefix: nil,
67+
old_api_secret_key: nil
6568
)
6669
ShopifyAPI::Context.setup(
6770
api_key: api_key ? api_key : ShopifyAPI::Context.api_key,
@@ -74,7 +77,8 @@ def modify_context(
7477
session_storage: session_storage ? session_storage : ShopifyAPI::Context.session_storage,
7578
logger: logger ? logger : ShopifyAPI::Context.logger,
7679
private_shop: private_shop != "do-not-set" ? private_shop : ShopifyAPI::Context.private_shop,
77-
user_agent_prefix: user_agent_prefix ? user_agent_prefix : ShopifyAPI::Context.user_agent_prefix
80+
user_agent_prefix: user_agent_prefix ? user_agent_prefix : ShopifyAPI::Context.user_agent_prefix,
81+
old_api_secret_key: old_api_secret_key ? old_api_secret_key : ShopifyAPI::Context.old_api_secret_key
7882
)
7983
end
8084
end

test/utils/session_utils_test.rb

+30
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ def test_fails_if_authorization_header_bearer_token_is_invalid
8484
end
8585
end
8686

87+
def test_fails_if_authorization_header_be
88+
modify_context(is_embedded: true)
89+
jwt_header = create_jwt_header("UNKNOWN_API_SECRET_KEY")
90+
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
91+
ShopifyAPI::Utils::SessionUtils.load_current_session(auth_header: jwt_header, is_online: true)
92+
end
93+
end
94+
95+
def test_decodes_jwt_token_signed_with_old_secret
96+
modify_context(is_embedded: true)
97+
modify_context(old_api_secret_key: "OLD_API_SECRET_KEY")
98+
jwt_header = create_jwt_header(ShopifyAPI::Context.old_api_secret_key)
99+
loaded_session = ShopifyAPI::Utils::SessionUtils.load_current_session(auth_header: jwt_header, is_online: true)
100+
assert_equal(@online_embedded_session, loaded_session)
101+
end
102+
103+
def test_fails_if_old_api_secret_key_is_invalid
104+
modify_context(is_embedded: true)
105+
modify_context(old_api_secret_key: "OLD_API_SECRET_KEY")
106+
jwt_header = create_jwt_header("UNKNOWN_OLD_API_SECRET_KEY")
107+
assert_raises(ShopifyAPI::Errors::InvalidJwtTokenError) do
108+
ShopifyAPI::Utils::SessionUtils.load_current_session(auth_header: jwt_header, is_online: true)
109+
end
110+
end
111+
87112
def test_fails_if_authorization_header_is_not_a_bearer_token
88113
modify_context(is_embedded: true)
89114
assert_raises(ShopifyAPI::Errors::MissingJwtTokenError) do
@@ -212,6 +237,11 @@ def add_session(is_online:)
212237
another_session = ShopifyAPI::Auth::Session.new(shop: @shop, is_online: is_online)
213238
ShopifyAPI::Context.session_storage.store_session(another_session)
214239
end
240+
241+
def create_jwt_header(api_secret_key)
242+
jwt_token = JWT.encode(@jwt_payload, api_secret_key, "HS256")
243+
"Bearer #{jwt_token}"
244+
end
215245
end
216246
end
217247
end

0 commit comments

Comments
 (0)