Skip to content

Commit 452931b

Browse files
committed
feat(hooks): Avoid timing attacks in hooks usage JS examples, and add the php ones
1 parent 0dfb189 commit 452931b

1 file changed

Lines changed: 39 additions & 14 deletions

File tree

docs/hooks.md

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ will contain:
2525

2626
```json
2727
{
28-
"user": {}, // Created user informations
28+
"user": {}, // Created user information
2929
"sender": {} // User that triggered the creation
3030
}
3131
```
@@ -37,7 +37,8 @@ It will contain the following payload:
3737

3838
```json
3939
{
40-
"user": {}, // Updated user
40+
"oldUser": {}, // The old user information
41+
"user": {}, // The new updated user
4142
"sender": {} // User that triggered the update
4243
}
4344
```
@@ -50,7 +51,7 @@ for the `leftAt` property to disable account.
5051

5152
```json
5253
{
53-
"userId": {}, // Deleted user id
54+
"user": {}, // The old user information
5455
"sender": {} // User that triggered the deletion
5556
}
5657
```
@@ -67,6 +68,16 @@ example integrations bellow:
6768
In NodeJS you'll need no any extra library to perform the validation since HMAC SHA1 can be performed from the
6869
`crypto` module of the NodeJS API.
6970

71+
```js
72+
const checkSignature = (secret, payload, actualHash) => {
73+
const body = typeof payload === 'string' ? payload : JSON.stringify(payload);
74+
const expectedHash = `sha1=${crypto.createHmac('sha1', secret).update(body).digest('hex')}`;
75+
76+
return expectedHash.length === actualHash.length && timingSafeEqual(new Buffer(expectedHash), new Buffer(actualHash))
77+
};
78+
```
79+
80+
7081
Example using Express:
7182

7283
```js
@@ -76,13 +87,9 @@ import crypto from 'crypto';
7687
const secret = 'TopSecretHookPassword@SuperStrong#123456';
7788
const app = express();
7889

79-
app.use((req, res, next) => {
80-
// If using body parser be sure to calculate from stringified version
81-
// const body = JSON.stringify(req.body);
82-
83-
const hash = crypto.createHmac('sha1', secret).update(req.body).digest('hex');
8490

85-
if (`sha1=${hash}` !== req.headers['x-lvconnect-signature']) {
91+
app.use(({ body, headers }, res, next) => {
92+
if (checkSignature(secret, body, headers['x-lvconnect-signature'])) {
8693
next(new Error('Invalid payload signature'));
8794
}
8895

@@ -106,11 +113,8 @@ server.auth.scheme('signature', (_, options) => ({
106113
credentials: { event: req.headers['x-lvconnect-event'], identifier: req.headers['x-lvconnect-delivery'] },
107114
artifacts: { signature: req.headers['x-lvconnect-signature'] },
108115
}),
109-
payload: async (req, h) => {
110-
const body = JSON.stringify(req.payload);
111-
const hash = crypto.createHmac('sha1', options.secret).update(body).digest('hex');
112-
113-
if (`sha1=${hash}` !== req.headers['x-lvconnect-signature']) {
116+
payload: async ({ payload, headers }, h) => {
117+
if (checkSignature(options.secret, payload, headers['x-lvconnect-signature'])) {
114118
throw Boom.forbidden('Invalid payload signature');
115119
}
116120

@@ -135,3 +139,24 @@ server.route({
135139

136140
server.start();
137141
```
142+
143+
### Php
144+
145+
```php
146+
<?php
147+
148+
use Symfony\Component\HttpFoundation\Request;
149+
150+
class CheckHookSignature
151+
{
152+
public function __invoke(string $secret, Request $request)
153+
{
154+
$payload = $request->getContent();
155+
$actualHash = $request->headers->get('x-lvconnect-signature');
156+
157+
$expectedHash = 'sha1=' . hash_hmac('sha1', $payload, $secret);
158+
159+
return hash_equals($expectedHash, $actualHash);
160+
}
161+
}
162+
```

0 commit comments

Comments
 (0)