Skip to content

Commit 99c9e62

Browse files
authored
Merge pull request #59 from rishavbhowmik/dev-webhook-signature
Webhook signature verification tool
2 parents d1cf79d + 9dab91e commit 99c9e62

File tree

8 files changed

+453
-3
lines changed

8 files changed

+453
-3
lines changed

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,105 @@ When you exceed the rate limits for an endpoint, you will receive a `429` status
11251125
| `X-RateLimit-Reset` | The amount of time in milliseconds before you can make another request to this endpoint. Pause/sleep your workflow for this duration. |
11261126
| `X-RateLimit-Interval` | The duration of interval in milliseconds for which this rate limit was exceeded. |
11271127

1128+
## Verify webhook events
1129+
1130+
ImageKit sends `x-ik-signature` in the webhook request header, which can be used to verify the authenticity of the webhook request.
1131+
1132+
Verifing webhook signature is easy with imagekit SDK. All you need is `x-ik-signature`, rawRequestBody and secretKey. You can copy webhook secret from imagekit dashboard.
1133+
1134+
```js
1135+
try {
1136+
const {
1137+
timestamp, // Unix timestamp in milliseconds
1138+
event, // Parsed webhook event object
1139+
} = imagekit.verifyWebhookEvent(
1140+
'{"type":"video.transformation.accepted","id":"58e6d24d-6098-4319-be8d-40c3cb0a402d"...', // Raw request body encoded as Uint8Array or UTF8 string
1141+
't=1655788406333,v1=d30758f47fcb31e1fa0109d3b3e2a6c623e699aaf1461cba6bd462ef58ea4b31', // Request header `x-ik-signature`
1142+
'whsec_...' // Webhook secret
1143+
)
1144+
// { timestamp: 1655788406333, event: {"type":"video.transformation.accepted","id":"58e6d24d-6098-4319-be8d-40c3cb0a402d"...} }
1145+
} catch (err) {
1146+
// `verifyWebhookEvent` will throw an error if the signature is invalid
1147+
// And the webhook event must be ignored and not processed
1148+
console.log(err);
1149+
// Under normal circumstances you may catch following errors:
1150+
// - `Error: Incorrect signature` - you may check the webhook secret & the request body being used.
1151+
// - `Error: Signature missing` or `Error: Timestamp missing` or `Error: Invalid timestamp` - you may check `x-ik-signature` header used.
1152+
}
1153+
```
1154+
1155+
Here is an example for implementing with express.js server.
1156+
1157+
```js
1158+
const express = require('express');
1159+
const Imagekit = require('imagekit');
1160+
1161+
// Webhook configs
1162+
const WEBHOOK_ENDPOINT = '/webhook';
1163+
const WEBHOOK_SECRET = 'whsec_...'; // Copy from Imagekit dashboard
1164+
const WEBHOOK_EXPIRY_DURATION = 60 * 1000; // 60 seconds
1165+
1166+
// Server configs
1167+
const PORT = 8081;
1168+
1169+
const imagekit = new Imagekit({
1170+
publicKey: 'pub_...',
1171+
urlEndpoint: 'https://ik.imagekit.io/example',
1172+
privateKey: 'pvt_...',
1173+
})
1174+
1175+
const app = express();
1176+
1177+
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
1178+
// Get `x-ik-signature` from webhook request header & rawRequestBody
1179+
const signature = req.headers["x-ik-signature"]; // eg. 't=1655788406333,v1=d30758f47fcb31e1fa0109d3b3e2a6c623e699aaf1461cba6bd462ef58ea4b31'
1180+
const rawBody = req.rawBody;// Unmodified request body encoded as Uint8Array or UTF8 string
1181+
1182+
// Verify signature & parse event
1183+
let webhookResult;
1184+
try {
1185+
webhookResult = imagekit.verifyWebhookEvent(rawBody, signature, WEBHOOK_SECRET);
1186+
// `verifyWebhookEvent` method will throw an error if signature is invalid
1187+
} catch (e) {
1188+
// Failed to verify webhook
1189+
return res.status(401).send(`Webhook error: ${e.message}`);
1190+
}
1191+
const { timestamp, event } = webhookResult;
1192+
1193+
// Check if webhook has expired
1194+
if (timestamp + WEBHOOK_EXPIRY_DURATION < Date.now()) {
1195+
return res.status(401).send('Webhook signature expired');
1196+
}
1197+
1198+
// Handle webhook
1199+
switch (event.type) {
1200+
case 'video.transformation.accepted':
1201+
// It is triggered when a new video transformation request is accepted for processing. You can use this for debugging purposes.
1202+
break;
1203+
case 'video.transformation.ready':
1204+
// It is triggered when a video encoding is finished and the transformed resource is ready to be served. You should listen to this webhook and update any flag in your database or CMS against that particular asset so your application can start showing it to users.
1205+
break;
1206+
case 'video.transformation.error':
1207+
// It is triggered if an error occurs during encoding. Listen to this webhook to log the reason. You should check your origin and URL-endpoint settings if the reason is related to download failure. If the reason seems like an error on the ImageKit side, then raise a support ticket at [email protected].
1208+
break;
1209+
// ... handle other event types
1210+
default:
1211+
console.log(`Unhandled event type ${event.type}`);
1212+
}
1213+
1214+
// Acknowledge webhook is received and processed successfully
1215+
res.status(200).end();
1216+
});
1217+
1218+
app.listen(PORT, () => {
1219+
console.log(`Server listening on port ${PORT}`);
1220+
console.log(
1221+
`Webhook endpoint: 'http://localhost:${PORT}${WEBHOOK_ENDPOINT}'`,
1222+
'Do replace 'localhost' with public endpoint'
1223+
);
1224+
});
1225+
```
1226+
11281227
## Support
11291228

11301229
For any feedback or to report any issues or general implementation support, please reach out to [[email protected]](mailto:[email protected])

index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { IKCallback } from "./libs/interfaces/IKCallback";
4040
import manage from "./libs/manage";
4141
import signature from "./libs/signature";
4242
import upload from "./libs/upload";
43+
import { verify as verifyWebhookEvent } from "./utils/webhook-signature";
4344
import customMetadataField from "./libs/manage/custom-metadata-field";
4445
/*
4546
Implementations
@@ -75,7 +76,6 @@ const promisify = function <T = void>(thisContext: ImageKit, fn: Function) {
7576
}
7677
};
7778
};
78-
7979
class ImageKit {
8080
options: ImageKitOptions = {
8181
uploadEndpoint: "https://upload.imagekit.io/api/v1/files/upload",
@@ -667,6 +667,14 @@ class ImageKit {
667667
pHashDistance(firstPHash: string, secondPHash: string): number | Error {
668668
return pHashUtils.pHashDistance(firstPHash, secondPHash);
669669
}
670+
671+
/**
672+
* @param payload - Raw webhook request body (Encoded as UTF8 string or Buffer)
673+
* @param signature - Webhook signature as UTF8 encoded strings (Stored in `x-ik-signature` header of the request)
674+
* @param secret - Webhook secret as UTF8 encoded string [Copy from ImageKit dashboard](https://imagekit.io/dashboard/developer/webhooks)
675+
* @returns \{ `timstamp`: Verified UNIX epoch timestamp if signature, `event`: Parsed webhook event payload \}
676+
*/
677+
verifyWebhookEvent = verifyWebhookEvent;
670678
}
671679

672-
export = ImageKit;
680+
export = ImageKit;

libs/constants/errorMessages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ export default {
4242
"INVALID_FILE_PATH": { message: "Invalid value for filePath", help: "Pass the full path of the file. For example - /path/to/file.jpg" },
4343
"INVALID_NEW_FILE_NAME": { message: "Invalid value for newFileName. It should be a string.", help: "" },
4444
"INVALID_PURGE_CACHE": { message: "Invalid value for purgeCache. It should be boolean.", help: "" },
45+
// Webhook signature
46+
"VERIFY_WEBHOOK_EVENT_SIGNATURE_INCORRECT": { message: "Incorrect signature", help: "Please pass x-ik-signature header as utf8 string" },
47+
"VERIFY_WEBHOOK_EVENT_SIGNATURE_MISSING": { message: "Signature missing", help: "Please pass x-ik-signature header as utf8 string" },
48+
"VERIFY_WEBHOOK_EVENT_TIMESTAMP_MISSING": { message: "Timestamp missing", help: "Please pass x-ik-signature header as utf8 string" },
49+
"VERIFY_WEBHOOK_EVENT_TIMESTAMP_INVALID": { message: "Timestamp invalid", help: "Please pass x-ik-signature header as utf8 string" },
4550
};

libs/interfaces/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import { MoveFolderOptions, MoveFolderResponse, MoveFolderError } from "./MoveFo
1717
import { DeleteFileVersionOptions, RestoreFileVersionOptions } from "./FileVersion"
1818
import { CreateCustomMetadataFieldOptions, CustomMetadataField, UpdateCustomMetadataFieldOptions, GetCustomMetadataFieldsOptions } from "./CustomMetatadaField"
1919
import { RenameFileOptions, RenameFileResponse } from "./Rename"
20+
import {
21+
WebhookEvent,
22+
WebhookEventVideoTransformationAccepted,
23+
WebhookEventVideoTransformationReady,
24+
WebhookEventVideoTransformationError,
25+
} from "./webhookEvent";
2026

2127
type FinalUrlOptions = ImageKitOptions & UrlOptions; // actual options used to construct url
2228

@@ -56,5 +62,9 @@ export type {
5662
UpdateCustomMetadataFieldOptions,
5763
RenameFileOptions,
5864
RenameFileResponse,
65+
WebhookEvent,
66+
WebhookEventVideoTransformationAccepted,
67+
WebhookEventVideoTransformationReady,
68+
WebhookEventVideoTransformationError,
5969
};
6070
export type { IKCallback } from "./IKCallback";

libs/interfaces/webhookEvent.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
type Asset = {
2+
url: string;
3+
};
4+
5+
type TransformationOptions = {
6+
video_codec: string;
7+
audio_codec: string;
8+
auto_rotate: boolean;
9+
quality: number;
10+
format: string;
11+
};
12+
13+
interface WebhookEventBase {
14+
type: string;
15+
id: string;
16+
created_at: string; // Date
17+
}
18+
19+
/** WebhookEvent for "video.transformation.*" type */
20+
interface WebhookEventVideoTransformationBase extends WebhookEventBase {
21+
request: {
22+
x_request_id: string;
23+
url: string;
24+
user_agent: string;
25+
};
26+
}
27+
28+
export interface WebhookEventVideoTransformationAccepted extends WebhookEventVideoTransformationBase {
29+
type: "video.transformation.accepted";
30+
data: {
31+
asset: Asset;
32+
transformation: {
33+
type: string;
34+
options: TransformationOptions;
35+
};
36+
};
37+
}
38+
39+
export interface WebhookEventVideoTransformationReady extends WebhookEventVideoTransformationBase {
40+
type: "video.transformation.ready";
41+
timings: {
42+
donwload_duration: number;
43+
encoding_duration: number;
44+
};
45+
data: {
46+
asset: Asset;
47+
transformation: {
48+
type: string;
49+
options: TransformationOptions;
50+
output: {
51+
url: string;
52+
video_metadata: {
53+
duration: number;
54+
width: number;
55+
height: number;
56+
bitrate: number;
57+
};
58+
};
59+
};
60+
};
61+
}
62+
63+
export interface WebhookEventVideoTransformationError extends WebhookEventVideoTransformationBase {
64+
type: "video.transformation.error";
65+
data: {
66+
asset: Asset;
67+
transformation: {
68+
type: string;
69+
options: TransformationOptions;
70+
error: {
71+
reason: string;
72+
};
73+
};
74+
};
75+
}
76+
77+
export type WebhookEvent =
78+
| WebhookEventVideoTransformationAccepted
79+
| WebhookEventVideoTransformationReady
80+
| WebhookEventVideoTransformationError;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "imagekit",
3-
"version": "4.0.1",
3+
"version": "4.1.0",
44
"description": "Offical NodeJS SDK for ImageKit.io integration",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

0 commit comments

Comments
 (0)