Skip to content

Commit c13978f

Browse files
authored
Merge pull request #168 from pusher/wh-validation
Add pusher signature validation
2 parents 478044d + 1499d0a commit c13978f

File tree

4 files changed

+132
-4
lines changed

4 files changed

+132
-4
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@ Using presence channels is similar to private channels, but you can specify extr
149149
$pusher->presence_auth('presence-my-channel','socket_id', 'user_id', 'user_info');
150150
```
151151

152+
## Webhooks
153+
154+
This library provides a way of verifying that webhooks you receive from Pusher are actually genuine webhooks from Pusher. It also provides a structure for storing them. A helper method called `webhook` enables this. Pass in the headers and body of the request, and it'll return a Webhook object with your verified events. If the library was unable to validate the signature, an exception is thrown instead.
155+
156+
```php
157+
$webhook = $pusher->webhook($request_headers, $request_body);
158+
$number_of_events = count($webhook->get_events());
159+
$time_recieved = $webhook->get_time_ms();
160+
```
161+
162+
152163
### Presence example
153164

154165
First set this variable in your JS app:

src/Pusher.php

+49-4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public function __construct($auth_key, $secret, $app_id, $options = array(), $ho
143143

144144
// ensure host doesn't have a scheme prefix
145145
$this->settings['host'] =
146-
preg_replace('/http[s]?\:\/\//', '', $this->settings['host'], 1);
146+
preg_replace('/http[s]?\:\/\//', '', $this->settings['host'], 1);
147147
}
148148

149149
/**
@@ -398,9 +398,15 @@ private function ddn_domain()
398398
*
399399
* @return string
400400
*/
401-
public static function build_auth_query_string($auth_key, $auth_secret, $request_method, $request_path,
402-
$query_params = array(), $auth_version = '1.0', $auth_timestamp = null)
403-
{
401+
public static function build_auth_query_string(
402+
$auth_key,
403+
$auth_secret,
404+
$request_method,
405+
$request_path,
406+
$query_params = array(),
407+
$auth_version = '1.0',
408+
$auth_timestamp = null
409+
) {
404410
$params = array();
405411
$params['auth_key'] = $auth_key;
406412
$params['auth_timestamp'] = (is_null($auth_timestamp) ? time() : $auth_timestamp);
@@ -745,4 +751,43 @@ public function notify($interests, $data = array(), $debug = false)
745751

746752
return false;
747753
}
754+
755+
/**
756+
* Verify that a webhook actually came from Pusher, and marshals them into a Webhook object.
757+
*
758+
* @param array $headers an array of headers from the request (for example, from getallheaders())
759+
* @param string $body the body of the request (for example, from file_get_contents('php://input'))
760+
*
761+
* @return Webhook object with the properties time_ms (an int) and events (an array of event objects)
762+
*/
763+
public function webhook($headers, $body)
764+
{
765+
$this->ensure_valid_signature($headers, $body);
766+
$decoded_json = json_decode($body);
767+
$webhookobj = new Webhook($decoded_json->time_ms, $decoded_json->events);
768+
769+
return $webhookobj;
770+
}
771+
772+
/**
773+
* Verify that a given Pusher Signature is valid.
774+
*
775+
* @param array $headers an array of headers from the request (for example, from getallheaders())
776+
* @param string $body the body of the request (for example, from file_get_contents('php://input'))
777+
*
778+
* @throws PusherException if signature is inccorrect.
779+
*/
780+
public function ensure_valid_signature($headers, $body)
781+
{
782+
$x_pusher_key = $headers['X-Pusher-Key'];
783+
$x_pusher_signature = $headers['X-Pusher-Signature'];
784+
if ($x_pusher_key == $this->settings['auth_key']) {
785+
$expected = hash_hmac('sha256', $body, $this->settings['secret']);
786+
if ($expected === $x_pusher_signature) {
787+
return;
788+
}
789+
}
790+
791+
throw new PusherException(sprintf('Received WebHook with invalid signature: got %s, expected %s.', $x_pusher_signature, $expected));
792+
}
748793
}

src/Webhook.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Pusher;
4+
5+
class Webhook
6+
{
7+
private $time_ms;
8+
private $events = array();
9+
10+
public function __construct($time_ms, $events)
11+
{
12+
$this->time_ms = $time_ms;
13+
$this->events = $events;
14+
}
15+
16+
public function get_events()
17+
{
18+
return $this->events;
19+
}
20+
21+
public function get_time_ms()
22+
{
23+
return $this->time_ms;
24+
}
25+
}

tests/unit/webhookTest.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
class webhookTest extends PHPUnit_Framework_TestCase
4+
{
5+
protected function setUp()
6+
{
7+
$this->pusher = new Pusher\Pusher('thisisaauthkey', 'thisisasecret', 1, true);
8+
$this->auth_key = 'thisisaauthkey';
9+
}
10+
11+
public function testValidWebhookSignature()
12+
{
13+
$signature = '40e0ad3b9aa49529322879e84de1aaaf18bde1efe839ca263d540cc865510d25';
14+
$body = '{"hello":"world"}';
15+
$headers = array(
16+
'X-Pusher-Key' => $this->auth_key,
17+
'X-Pusher-Signature' => $signature,
18+
);
19+
20+
$this->pusher->ensure_valid_signature($headers, $body);
21+
}
22+
23+
/**
24+
* @expectedException \Pusher\PusherException
25+
*/
26+
public function testInvalidWebhookSignature()
27+
{
28+
$signature = 'potato';
29+
$body = '{"hello":"world"}';
30+
$headers = array(
31+
'X-Pusher-Key' => $this->auth_key,
32+
'X-Pusher-Signature' => $signature,
33+
);
34+
$wrong_signature = $this->pusher->ensure_valid_signature($headers, $body);
35+
}
36+
37+
public function testDecodeWebhook()
38+
{
39+
$headers_json = '{"X-Pusher-Key":"'.$this->auth_key.'","X-Pusher-Signature":"a19cab2af3ca1029257570395e78d5d675e9e700ca676d18a375a7083178df1c"}';
40+
$body = '{"time_ms":1530710011901,"events":[{"name":"client_event","channel":"private-my-channel","event":"client-event","data":"Unencrypted","socket_id":"240621.35780774"}]}';
41+
$headers = json_decode($headers_json, true);
42+
43+
$decodedWebhook = $this->pusher->webhook($headers, $body);
44+
$this->assertEquals($decodedWebhook->get_time_ms(), 1530710011901);
45+
$this->assertEquals(count($decodedWebhook->get_events()), 1);
46+
}
47+
}

0 commit comments

Comments
 (0)