Skip to content

Commit d1e09e2

Browse files
lizkenyonclaude
andcommitted
Add support for new webhook header format
Update webhook verification to support both legacy (X-Shopify-*) and new (shopify-*) header formats. New headers take precedence when both are present, with fallback to legacy format for backward compatibility. Changes: - Add shopify_header helper in PayloadVerification for dual header support - Update shop_domain in WebhookVerification to use the helper - Add use_new_headers option to test helpers - Add unit tests for new header format verification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e7ae8e5 commit d1e09e2

File tree

7 files changed

+103
-16
lines changed

7 files changed

+103
-16
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ GEM
271271
ruby-progressbar (1.13.0)
272272
ruby2_keywords (0.0.5)
273273
securerandom (0.4.1)
274-
shopify_api (16.0.0)
274+
shopify_api (16.1.0)
275275
activesupport
276276
concurrent-ruby
277277
hash_diff

lib/shopify_app/controller_concerns/payload_verification.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module PayloadVerification
77
private
88

99
def shopify_hmac
10-
request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"]
10+
shopify_header("hmac-sha256")
1111
end
1212

1313
def hmac_valid?(data)
@@ -21,5 +21,12 @@ def hmac_valid?(data)
2121
)
2222
end
2323
end
24+
25+
# Retrieves Shopify headers with fallback to legacy format.
26+
# New headers (shopify-*) take precedence over legacy (X-Shopify-*).
27+
def shopify_header(name)
28+
formatted_name = name.upcase.tr("-", "_")
29+
request.headers["HTTP_SHOPIFY_#{formatted_name}"] || request.headers["HTTP_X_SHOPIFY_#{formatted_name}"]
30+
end
2431
end
2532
end

lib/shopify_app/controller_concerns/webhook_verification.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def verify_request
2121
end
2222

2323
def shop_domain
24-
request.headers["HTTP_X_SHOPIFY_SHOP_DOMAIN"]
24+
shopify_header("shop-domain")
2525
end
2626
end
2727
end

lib/shopify_app/test_helpers/webhook_verification_helper.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
module ShopifyApp
44
module TestHelpers
55
module WebhookVerificationHelper
6-
def authorized_webhook_verification_headers!(params = {})
6+
def authorized_webhook_verification_headers!(params = {}, use_new_headers: false)
77
digest = OpenSSL::Digest.new("sha256")
88
secret = ShopifyApp.configuration.secret
99
valid_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, params.to_query)).strip
10-
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = valid_hmac
10+
header_key = use_new_headers ? "HTTP_SHOPIFY_HMAC_SHA256" : "HTTP_X_SHOPIFY_HMAC_SHA256"
11+
@request.headers[header_key] = valid_hmac
1112
end
1213

13-
def unauthorized_webhook_verification_headers!
14-
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = "invalid_hmac"
14+
def unauthorized_webhook_verification_headers!(use_new_headers: false)
15+
header_key = use_new_headers ? "HTTP_SHOPIFY_HMAC_SHA256" : "HTTP_X_SHOPIFY_HMAC_SHA256"
16+
@request.headers[header_key] = "invalid_hmac"
1517
end
1618
end
1719
end

test/controllers/extension_verification_controller_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ class ExtensionVerificationControllerTest < ActionController::TestCase
3636
end
3737
end
3838

39+
test "responds ok when hmac is correct with new header format" do
40+
with_application_test_routes do
41+
params = { foo: "anything" }
42+
valid_hmac = "yCGX/RrK4fcuNtr3ztk5tQGsOBjcAzHpGLdMUrbV8yI=" # Valid hmac using the new secret
43+
@request.headers["HTTP_SHOPIFY_HMAC_SHA256"] = valid_hmac
44+
post "extension_action", params: params
45+
assert_response :ok
46+
end
47+
end
48+
49+
test "return unauthorized when hmac is incorrect with new header format" do
50+
with_application_test_routes do
51+
@request.headers["HTTP_SHOPIFY_HMAC_SHA256"] = "invalid_hmac"
52+
post :extension_action, params: { foo: "anything" }
53+
assert_response :unauthorized
54+
end
55+
end
56+
3957
private
4058

4159
def with_application_test_routes

test/integration/webhooks_controller_test.rb

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,49 @@ class WebhooksControllerTest < ActionDispatch::IntegrationTest
3535
end
3636
end
3737

38+
test "receives webhook with new header format and calls process" do
39+
ShopifyAPI::Webhooks::Registry.stubs(:process).returns(nil)
40+
ShopifyAPI::Webhooks::Registry.expects(:process).once
41+
send_webhook "order_update", { foo: :bar }, use_new_headers: true
42+
assert_response :ok
43+
end
44+
45+
test "receives webhook with legacy header format and calls process" do
46+
ShopifyAPI::Webhooks::Registry.stubs(:process).returns(nil)
47+
ShopifyAPI::Webhooks::Registry.expects(:process).once
48+
send_webhook "order_update", { foo: :bar }, use_new_headers: false
49+
assert_response :ok
50+
end
51+
3852
private
3953

40-
def send_webhook(name, data)
54+
def send_webhook(name, data, use_new_headers: false)
4155
post(
4256
shopify_app.webhooks_path(name),
4357
params: data,
44-
headers: headers(name),
58+
headers: headers(name, use_new_headers: use_new_headers),
4559
)
4660
end
4761

48-
def headers(name)
62+
def headers(name, use_new_headers: false)
4963
hmac = OpenSSL::HMAC.digest(
5064
OpenSSL::Digest.new("sha256"),
5165
"API_SECRET_KEY",
5266
"{}",
5367
)
54-
headers = {
55-
"x-shopify-topic" => name,
56-
"x-shopify-hmac-sha256" => Base64.encode64(hmac),
57-
"x-shopify-shop-domain" => "test.myshopify.com",
58-
}
59-
headers
68+
if use_new_headers
69+
{
70+
"shopify-topic" => name,
71+
"shopify-hmac-sha256" => Base64.encode64(hmac),
72+
"shopify-shop-domain" => "test.myshopify.com",
73+
}
74+
else
75+
{
76+
"x-shopify-topic" => name,
77+
"x-shopify-hmac-sha256" => Base64.encode64(hmac),
78+
"x-shopify-shop-domain" => "test.myshopify.com",
79+
}
80+
end
6081
end
6182
end
6283
end

test/shopify_app/controller_concerns/webhook_verification_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,45 @@ class WebhookVerificationTest < ActionController::TestCase
6363
end
6464
end
6565

66+
test "authorized request with new header format should be successful" do
67+
with_application_test_routes do
68+
params = { foo: "anything" }
69+
valid_hmac = "yCGX/RrK4fcuNtr3ztk5tQGsOBjcAzHpGLdMUrbV8yI=" # Valid hmac using the new secret
70+
@request.headers["HTTP_SHOPIFY_HMAC_SHA256"] = valid_hmac
71+
post :webhook_action, params: params
72+
assert_response :ok
73+
end
74+
end
75+
76+
test "un-verified request with new header format returns unauthorized" do
77+
with_application_test_routes do
78+
@request.headers["HTTP_SHOPIFY_HMAC_SHA256"] = "invalid_hmac"
79+
post :webhook_action, params: { foo: "anything" }
80+
assert_response :unauthorized
81+
end
82+
end
83+
84+
test "new header format takes precedence when both headers present" do
85+
with_application_test_routes do
86+
params = { foo: "anything" }
87+
valid_hmac = "yCGX/RrK4fcuNtr3ztk5tQGsOBjcAzHpGLdMUrbV8yI=" # Valid hmac
88+
@request.headers["HTTP_SHOPIFY_HMAC_SHA256"] = valid_hmac # New format (valid)
89+
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = "invalid_hmac" # Legacy format (invalid)
90+
post :webhook_action, params: params
91+
assert_response :ok
92+
end
93+
end
94+
95+
test "falls back to legacy header when new header not present" do
96+
with_application_test_routes do
97+
params = { foo: "anything" }
98+
valid_hmac = "yCGX/RrK4fcuNtr3ztk5tQGsOBjcAzHpGLdMUrbV8yI=" # Valid hmac
99+
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = valid_hmac # Only legacy header
100+
post :webhook_action, params: params
101+
assert_response :ok
102+
end
103+
end
104+
66105
private
67106

68107
def with_application_test_routes

0 commit comments

Comments
 (0)