Skip to content

Commit

Permalink
- Fix timestamp format in signature verification
Browse files Browse the repository at this point in the history
- Deprecate USER_FOLLOW since it's now CHANNEL_FOLLOW.
- Improve documentation
  • Loading branch information
ghostzero committed Feb 6, 2021
1 parent bb1bf5e commit 1606dd8
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 21 deletions.
69 changes: 61 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ TWITCH_HELIX_SECRET=
TWITCH_HELIX_REDIRECT_URI=http://localhost
```

If you want to use the EventSub with the Webhook transport, then you are required to define a secret. This secret is a string between 10 and 100 characters.

```
TWITCH_HELIX_EVENTSUB_SECRET=
```

## Examples

### Basic
Expand Down Expand Up @@ -220,27 +226,47 @@ $result->insertUsers($twitch, 'from_id', 'from_user');

### Defining EventSub Handlers

By default, the EventSub webhook controller will automatically handle all EventSub notification and revocation calls;
however, if you have additional webhook events you would like to handle, you may do so by extending the EventSub webhook
controller.

To ensure your application can handle EventSub webhooks, be sure to configure the webhook callback url in the transport
payload.

Your controller's method names should correspond to Laravel Twitch's controller conventions. Specifically, methods
should be prefixed with `handle`, suffixed with `Notification` and the "camel case" name of the EventSub Type you wish
to handle. For example, if you wish to handle the `channel.follow` type, you should add a
`handleChannelFollowNotification` method to the controller:

```php
<?php

namespace App\Http\Controllers;

use romanzipp\Twitch\Http\Controllers\EventSubController as BaseController;
use Symfony\Component\HttpFoundation\Response;

class EventSubController extends BaseController
{
protected function handleNotification(array $payload)
public function handleChannelFollowNotification(array $payload): Response
{
return $this->successMethod();
return $this->successMethod(); // handle the channel follow notification...
}

protected function handleNotification(array $payload): Response
{
return $this->successMethod(); // handle all other incoming notifications...
}

protected function handleRevocation(array $payload)
protected function handleRevocation(array $payload): Response
{
return $this->successMethod();
return $this->successMethod(); // handle the subscription revocation...
}
}
```

Next, define a route to your EventSub webhook controller within your application's `routes/api.php` file.

```php
use App\Http\Controllers\EventSubController;

Expand All @@ -250,7 +276,9 @@ Route::post(
);
```

### Subscribe EventSub Events
### Create EventSub Subscription

> **Important**: When creating a subscription, you must specify a secret for purposes of verification, described above in “Configuration”. This secret is automatically attached to the webhook transport if it is not explicitly defined.
```php
use romanzipp\Twitch\Enums\EventSubType;
Expand All @@ -259,20 +287,45 @@ use romanzipp\Twitch\Twitch;
$twitch = new Twitch;


$twitch->subscribeEventSub([
$twitch->subscribeEventSub([], [
'type' => EventSubType::STREAM_ONLINE,
'version' => '1',
'condition' => [
'broadcaster_user_id' => '1337',
],
'transport' => [
'method' => 'webhook',
'callback' => 'https://example.com/webhooks/callback',
'secret' => 's3cRe7',
'callback' => 'https://example.com/api/twitch/eventsub/webhook',
]
]);
```

### List EventSub Subscription

```php
use romanzipp\Twitch\Twitch;

$twitch = new Twitch;

$result = $twitch->getEventSubs(['status' => 'notification_failures_exceeded']);

foreach ($result->data() as $item) {
// process the subscription
}
```

### Delete EventSub Subscription

```php
use romanzipp\Twitch\Twitch;

$twitch = new Twitch;

$twitch->unsubscribeEventSub([
'id' => '932b34ad-821a-490f-af43-b327187d0f5c'
]);
```

## Documentation

**Twitch Helix API Documentation: https://dev.twitch.tv/docs/api/reference**
Expand Down
69 changes: 61 additions & 8 deletions README.stub.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ TWITCH_HELIX_SECRET=
TWITCH_HELIX_REDIRECT_URI=http://localhost
```

If you want to use the EventSub with the Webhook transport, then you are required to define a secret. This secret is a string between 10 and 100 characters.

```
TWITCH_HELIX_EVENTSUB_SECRET=
```

## Examples

### Basic
Expand Down Expand Up @@ -220,27 +226,47 @@ $result->insertUsers($twitch, 'from_id', 'from_user');

### Defining EventSub Handlers

By default, the EventSub webhook controller will automatically handle all EventSub notification and revocation calls;
however, if you have additional webhook events you would like to handle, you may do so by extending the EventSub webhook
controller.

To ensure your application can handle EventSub webhooks, be sure to configure the webhook callback url in the transport
payload.

Your controller's method names should correspond to Laravel Twitch's controller conventions. Specifically, methods
should be prefixed with `handle`, suffixed with `Notification` and the "camel case" name of the EventSub Type you wish
to handle. For example, if you wish to handle the `channel.follow` type, you should add a
`handleChannelFollowNotification` method to the controller:

```php
<?php

namespace App\Http\Controllers;

use romanzipp\Twitch\Http\Controllers\EventSubController as BaseController;
use Symfony\Component\HttpFoundation\Response;

class EventSubController extends BaseController
{
protected function handleNotification(array $payload)
public function handleChannelFollowNotification(array $payload): Response
{
return $this->successMethod();
return $this->successMethod(); // handle the channel follow notification...
}

protected function handleNotification(array $payload): Response
{
return $this->successMethod(); // handle all other incoming notifications...
}

protected function handleRevocation(array $payload)
protected function handleRevocation(array $payload): Response
{
return $this->successMethod();
return $this->successMethod(); // handle the subscription revocation...
}
}
```

Next, define a route to your EventSub webhook controller within your application's `routes/api.php` file.

```php
use App\Http\Controllers\EventSubController;

Expand All @@ -250,7 +276,9 @@ Route::post(
);
```

### Subscribe EventSub Events
### Create EventSub Subscription

> **Important**: When creating a subscription, you must specify a secret for purposes of verification, described above in “Configuration”. This secret is automatically attached to the webhook transport if it is not explicitly defined.
```php
use romanzipp\Twitch\Enums\EventSubType;
Expand All @@ -259,20 +287,45 @@ use romanzipp\Twitch\Twitch;
$twitch = new Twitch;


$twitch->subscribeEventSub([
$twitch->subscribeEventSub([], [
'type' => EventSubType::STREAM_ONLINE,
'version' => '1',
'condition' => [
'broadcaster_user_id' => '1337',
],
'transport' => [
'method' => 'webhook',
'callback' => 'https://example.com/webhooks/callback',
'secret' => 's3cRe7',
'callback' => 'https://example.com/api/twitch/eventsub/webhook',
]
]);
```

### List EventSub Subscription

```php
use romanzipp\Twitch\Twitch;

$twitch = new Twitch;

$result = $twitch->getEventSubs(['status' => 'notification_failures_exceeded']);

foreach ($result->data() as $item) {
// process the subscription
}
```

### Delete EventSub Subscription

```php
use romanzipp\Twitch\Twitch;

$twitch = new Twitch;

$twitch->unsubscribeEventSub([
'id' => '932b34ad-821a-490f-af43-b327187d0f5c'
]);
```

## Documentation

**Twitch Helix API Documentation: https://dev.twitch.tv/docs/api/reference**
Expand Down
9 changes: 7 additions & 2 deletions src/Enums/EventSubType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ class EventSubType
// Triggers whenever a user updates their account.
public const USER_UPDATE = 'user.update';

// Triggers whenever a user follows another user.
public const USER_FOLLOW = 'user.follow';
/**
* @deprecated Please use the CHANNEL_FOLLOW constant
*/
public const USER_FOLLOW = 'channel.follow';

// Triggers whenever a broadcaster starts their stream.
public const STREAM_ONLINE = 'stream.online';

// Triggers whenever a broadcaster stops their stream.
public const STREAM_OFFLINE = 'stream.offline';

// Triggers whenever a user follows to a broadcaster's channel.
public const CHANNEL_FOLLOW = 'channel.follow';

// Triggers whenever a broadcaster updates their channel.
public const CHANNEL_UPDATE = 'channel.update';

Expand Down
29 changes: 27 additions & 2 deletions src/Objects/EventSubSignature.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

namespace romanzipp\Twitch\Objects;

use DateTime;
use romanzipp\Twitch\Exceptions\SignatureVerificationException;
use Symfony\Component\HttpFoundation\HeaderBag;

class EventSubSignature
{
/**
* Represents the timestamp format of twitch's eventsub api.
*/
const TIMESTAMP_PATTERN = '/^(\d+)-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):([0-5]\d):([0-5]\d|60)(\.\d+)?(([Zz])|([+|\-]([01]\d|2[0-3])))$/';

/**
* Verifies the signature header sent by Twitch. Throws an SignatureVerificationException
* exception if the verification fails for any reason.
Expand All @@ -22,7 +28,8 @@ class EventSubSignature
*/
public static function verifyHeader(string $payload, HeaderBag $headers, string $secret, int $tolerance = 60): void
{
$timestamp = $headers->get('twitch-eventsub-message-timestamp');
$rawTimestamp = $headers->get('twitch-eventsub-message-timestamp');
$timestamp = self::getTimestamp($rawTimestamp);

if ( ! is_numeric($timestamp)) {
throw new SignatureVerificationException('Unable to extract timestamp and signatures from header');
Expand All @@ -33,10 +40,28 @@ public static function verifyHeader(string $payload, HeaderBag $headers, string
}

$messageId = $headers->get('twitch-eventsub-message-id');
$expectedSignature = hash_hmac('sha256', $messageId . $timestamp . $payload, $secret);
$expectedSignature = hash_hmac('sha256', $messageId . $rawTimestamp . $payload, $secret);

if ($headers->get('twitch-eventsub-message-signature') !== $expectedSignature) {
throw new SignatureVerificationException();
}
}

private static function getTimestamp(?string $rawTimestamp): ?int
{
if ( ! $rawTimestamp) {
return null;
}

if ( ! preg_match(self::TIMESTAMP_PATTERN, $rawTimestamp, $m)) {
return null;
}

$dateTime = DateTime::createFromFormat(
'Y-m-d\TH:i:s.u\Z',
"$m[1]-$m[2]-$m[3]T$m[4]:$m[5]:$m[6]" . substr($m[7], 0, 6) . 'Z'
);

return $dateTime->getTimestamp();
}
}
6 changes: 5 additions & 1 deletion tests/VerifyEventSubSignatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ public function testAppAbortsWhenSecretDoesNotMatch(): void

private function withTimestamp($timestamp): void
{
$this->timestamp = $timestamp;
if (is_string($timestamp)) {
$this->timestamp = $timestamp;
} else {
$this->timestamp = date('Y-m-d\TH:i:s.u\Z', $timestamp);
}
}

private function withSignedSignature($secret): self
Expand Down

0 comments on commit 1606dd8

Please sign in to comment.