Skip to content

Commit 3dc86b3

Browse files
committed
Webhook verification working
1 parent 9da595a commit 3dc86b3

File tree

7 files changed

+102
-15
lines changed

7 files changed

+102
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Open `app/Http/Kernel.php` find `routeMiddleware` array. Add a new line with:
7878

7979
```php
8080
'auth.shop' => \OhMyBrew\ShopifyApp\Middleware\AuthShop::class,
81+
'auth.webhook' => \OhMyBrew\ShopifyApp\Middleware\AuthWebhook::class,
8182
```
8283

8384
### Jobs

docs/creating-webhooks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,5 @@ Route::post(
7878
'/webhook/some-string-here',
7979
'App\Http\Controllers\CustomWebhookController'
8080
)
81+
->middleware('auth.middleware')
8182
```

src/ShopifyApp/Middleware/AuthWebhook.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ class AuthWebhook
1515
*/
1616
public function handle(Request $request, Closure $next)
1717
{
18-
// ... validate HMAC and headers
18+
$hmac = request()->header('x-shopify-hmac-sha256');
19+
$shop = request()->header('x-shopify-shop-domain');
20+
$data = request()->getContent();
21+
22+
$hmacLocal = hash_hmac('sha256', $data, config('shopify-app.api_secret'));
23+
if ($hmac !== $hmacLocal || empty($shop)) {
24+
// Issue with HMAC or missing shop header
25+
abort(401, 'Invalid webhook signature');
26+
}
27+
28+
// All good, process webhook
29+
return $next($request);
1930
}
2031
}

src/ShopifyApp/resources/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@
7070
'/webhook/{type}',
7171
'OhMyBrew\ShopifyApp\Controllers\WebhookController@handle'
7272
)
73+
->middleware('auth.webhook')
7374
->name('webhook');
7475
});

tests/AuthWebhookMiddlewareTest.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,67 @@
11
<?php namespace OhMyBrew\ShopifyApp\Test;
22

33
use OhMyBrew\ShopifyApp\Middleware\AuthWebhook;
4-
use Illuminate\Support\Facades\Input;
4+
use Illuminate\Support\Facades\Queue;
5+
6+
if (!class_exists('App\Jobs\OrdersCreateJob')) {
7+
require 'OrdersCreateJobStub.php';
8+
}
59

610
class AuthWebhookMiddlewareTest extends TestCase
711
{
12+
/**
13+
* @expectedException Symfony\Component\HttpKernel\Exception\HttpException
14+
* @expectedExceptionMessage Invalid webhook signature
15+
*/
16+
public function testDenysForMissingShopHeader()
17+
{
18+
request()->header('x-shopify-hmac-sha256', '1234');
19+
(new AuthWebhook)->handle(request(), function($request) { });
20+
}
21+
22+
/**
23+
* @expectedException Symfony\Component\HttpKernel\Exception\HttpException
24+
* @expectedExceptionMessage Invalid webhook signature
25+
*/
26+
public function testDenysForMissingHmacHeader()
27+
{
28+
request()->header('x-shopify-shop-domain', 'example.myshopify.com');
29+
(new AuthWebhook)->handle(request(), function($request) { });
30+
}
31+
832
public function testRuns()
933
{
10-
$called = false;
11-
$result = (new AuthWebhook)->handle(request(), function($request) use(&$called) {
12-
$called = true;
13-
});
34+
Queue::fake();
35+
36+
$response = $this->call(
37+
'post',
38+
'/webhook/orders-create',
39+
[], [], [],
40+
[
41+
'HTTP_CONTENT_TYPE' => 'application/json',
42+
'HTTP_X_SHOPIFY_SHOP_DOMAIN' => 'example.myshopify.com',
43+
'HTTP_X_SHOPIFY_HMAC_SHA256' => '8432614ea1ce63b77959195b0e5e1e8469bfb7890e40ab51fb9c3ac26f8b050c', // Matches fixture data and API secret
44+
],
45+
file_get_contents(__DIR__.'/fixtures/webhook.json')
46+
);
47+
$response->assertStatus(201);
48+
}
1449

15-
$this->assertEquals($called);
50+
public function testInvalidHmacWontRun()
51+
{
52+
Queue::fake();
53+
54+
$response = $this->call(
55+
'post',
56+
'/webhook/orders-create',
57+
[], [], [],
58+
[
59+
'HTTP_CONTENT_TYPE' => 'application/json',
60+
'HTTP_X_SHOPIFY_SHOP_DOMAIN' => 'example.myshopify.com',
61+
'HTTP_X_SHOPIFY_HMAC_SHA256' => '8432614ea1ce63b77959195b0e5e1e8469bfb7890e40ab51fb9c3ac26f8b050c', // Matches fixture data and API secret
62+
],
63+
file_get_contents(__DIR__.'/fixtures/webhook.json') . 'invalid'
64+
);
65+
$response->assertStatus(401);
1666
}
1767
}

tests/Kernel.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Kernel extends \Orchestra\Testbench\Http\Kernel
1818
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
1919

2020
// Added for testing
21-
'auth.shop' => \OhMyBrew\ShopifyApp\Middleware\AuthShop::class
21+
'auth.shop' => \OhMyBrew\ShopifyApp\Middleware\AuthShop::class,
22+
'auth.webhook' => \OhMyBrew\ShopifyApp\Middleware\AuthWebhook::class
2223
];
2324
}

tests/WebhookControllerTest.php

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,34 @@
33
use \ReflectionMethod;
44
use Illuminate\Support\Facades\Queue;
55

6-
require 'OrdersCreateJobStub.php';
6+
if (!class_exists('App\Jobs\OrdersCreateJob')) {
7+
require 'OrdersCreateJobStub.php';
8+
}
79

810
class WebhookControllerTest extends TestCase
911
{
12+
public function setUp()
13+
{
14+
parent::setUp();
15+
16+
$this->headers = [
17+
'HTTP_CONTENT_TYPE' => 'application/json',
18+
'HTTP_X_SHOPIFY_SHOP_DOMAIN' => 'example.myshopify.com',
19+
'HTTP_X_SHOPIFY_HMAC_SHA256' => '8432614ea1ce63b77959195b0e5e1e8469bfb7890e40ab51fb9c3ac26f8b050c', // Matches fixture data and API secret
20+
];
21+
}
22+
1023
public function testShouldReturn201ResponseOnSuccess()
1124
{
1225
Queue::fake();
1326

14-
$response = $this->call('post', '/webhook/orders-create');
27+
$response = $this->call(
28+
'post',
29+
'/webhook/orders-create',
30+
[], [], [],
31+
$this->headers,
32+
file_get_contents(__DIR__.'/fixtures/webhook.json')
33+
);
1534
$response->assertStatus(201);
1635

1736
Queue::assertPushed(\App\Jobs\OrdersCreateJob::class);
@@ -20,7 +39,13 @@ public function testShouldReturn201ResponseOnSuccess()
2039

2140
public function testShouldReturnErrorResponseOnFailure()
2241
{
23-
$response = $this->call('post', '/webhook/products-create');
42+
$response = $this->call(
43+
'post',
44+
'/webhook/products-create',
45+
[], [], [],
46+
$this->headers,
47+
file_get_contents(__DIR__.'/fixtures/webhook.json')
48+
);
2449
$response->assertStatus(500);
2550
$this->assertEquals('Missing webhook job: \App\Jobs\ProductsCreateJob', $response->exception->getMessage());
2651
}
@@ -50,10 +75,7 @@ public function testWebhookShouldRecieveData()
5075
'post',
5176
'/webhook/orders-create',
5277
[], [], [],
53-
[
54-
'HTTP_CONTENT_TYPE' => 'application/json',
55-
'HTTP_X_SHOPIFY_SHOP_DOMAIN' => 'example.myshopify.com'
56-
],
78+
$this->headers,
5779
file_get_contents(__DIR__.'/fixtures/webhook.json')
5880
);
5981
$response->assertStatus(201);

0 commit comments

Comments
 (0)