test: dedupe telnyx webhook test fixtures

This commit is contained in:
Peter Steinberger 2026-03-13 21:35:50 +00:00
parent d5d2fe1b0e
commit b5010719d6
2 changed files with 104 additions and 134 deletions

View File

@ -22,6 +22,34 @@ function decodeBase64Url(input: string): Buffer {
return Buffer.from(padded, "base64");
}
function createSignedTelnyxCtx(params: {
privateKey: crypto.KeyObject;
rawBody: string;
}): WebhookContext {
const timestamp = String(Math.floor(Date.now() / 1000));
const signedPayload = `${timestamp}|${params.rawBody}`;
const signature = crypto
.sign(null, Buffer.from(signedPayload), params.privateKey)
.toString("base64");
return createCtx({
rawBody: params.rawBody,
headers: {
"telnyx-signature-ed25519": signature,
"telnyx-timestamp": timestamp,
},
});
}
function expectReplayVerification(
results: Array<{ ok: boolean; isReplay?: boolean; verifiedRequestKey?: string }>,
) {
expect(results.map((result) => result.ok)).toEqual([true, true]);
expect(results.map((result) => Boolean(result.isReplay))).toEqual([false, true]);
expect(results[0]?.verifiedRequestKey).toEqual(expect.any(String));
expect(results[1]?.verifiedRequestKey).toBe(results[0]?.verifiedRequestKey);
}
function expectWebhookVerificationSucceeds(params: {
publicKey: string;
privateKey: crypto.KeyObject;
@ -35,20 +63,8 @@ function expectWebhookVerificationSucceeds(params: {
event_type: "call.initiated",
payload: { call_control_id: "x" },
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signedPayload = `${timestamp}|${rawBody}`;
const signature = crypto
.sign(null, Buffer.from(signedPayload), params.privateKey)
.toString("base64");
const result = provider.verifyWebhook(
createCtx({
rawBody,
headers: {
"telnyx-signature-ed25519": signature,
"telnyx-timestamp": timestamp,
},
}),
createSignedTelnyxCtx({ privateKey: params.privateKey, rawBody }),
);
expect(result.ok).toBe(true);
}
@ -117,26 +133,12 @@ describe("TelnyxProvider.verifyWebhook", () => {
payload: { call_control_id: "call-replay-test" },
nonce: crypto.randomUUID(),
});
const timestamp = String(Math.floor(Date.now() / 1000));
const signedPayload = `${timestamp}|${rawBody}`;
const signature = crypto.sign(null, Buffer.from(signedPayload), privateKey).toString("base64");
const ctx = createCtx({
rawBody,
headers: {
"telnyx-signature-ed25519": signature,
"telnyx-timestamp": timestamp,
},
});
const ctx = createSignedTelnyxCtx({ privateKey, rawBody });
const first = provider.verifyWebhook(ctx);
const second = provider.verifyWebhook(ctx);
expect(first.ok).toBe(true);
expect(first.isReplay).toBeFalsy();
expect(first.verifiedRequestKey).toBeTruthy();
expect(second.ok).toBe(true);
expect(second.isReplay).toBe(true);
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
expectReplayVerification([first, second]);
});
});

View File

@ -98,6 +98,51 @@ function expectReplayResultPair(
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
}
function expectAcceptedWebhookVersion(
result: { ok: boolean; version?: string },
version: "v2" | "v3",
) {
expect(result).toMatchObject({ ok: true, version });
}
function verifyTwilioNgrokLoopback(signature: string) {
return verifyTwilioWebhook(
{
headers: {
host: "127.0.0.1:3334",
"x-forwarded-proto": "https",
"x-forwarded-host": "local.ngrok-free.app",
"x-twilio-signature": signature,
},
rawBody: "CallSid=CS123&CallStatus=completed&From=%2B15550000000",
url: "http://127.0.0.1:3334/voice/webhook",
method: "POST",
remoteAddress: "127.0.0.1",
},
"test-auth-token",
{ allowNgrokFreeTierLoopbackBypass: true },
);
}
function verifyTwilioSignedRequest(params: {
headers: Record<string, string>;
rawBody: string;
authToken: string;
publicUrl: string;
}) {
return verifyTwilioWebhook(
{
headers: params.headers,
rawBody: params.rawBody,
url: "http://local/voice/webhook?callId=abc",
method: "POST",
query: { callId: "abc" },
},
params.authToken,
{ publicUrl: params.publicUrl },
);
}
describe("verifyPlivoWebhook", () => {
it("accepts valid V2 signature", () => {
const authToken = "test-auth-token";
@ -127,8 +172,7 @@ describe("verifyPlivoWebhook", () => {
authToken,
);
expect(result.ok).toBe(true);
expect(result.version).toBe("v2");
expectAcceptedWebhookVersion(result, "v2");
});
it("accepts valid V3 signature (including multi-signature header)", () => {
@ -161,8 +205,7 @@ describe("verifyPlivoWebhook", () => {
authToken,
);
expect(result.ok).toBe(true);
expect(result.version).toBe("v3");
expectAcceptedWebhookVersion(result, "v3");
});
it("rejects missing signatures", () => {
@ -317,35 +360,10 @@ describe("verifyTwilioWebhook", () => {
"i-twilio-idempotency-token": "idem-replay-1",
};
const first = verifyTwilioWebhook(
{
headers,
rawBody: postBody,
url: "http://local/voice/webhook?callId=abc",
method: "POST",
query: { callId: "abc" },
},
authToken,
{ publicUrl },
);
const second = verifyTwilioWebhook(
{
headers,
rawBody: postBody,
url: "http://local/voice/webhook?callId=abc",
method: "POST",
query: { callId: "abc" },
},
authToken,
{ publicUrl },
);
const first = verifyTwilioSignedRequest({ headers, rawBody: postBody, authToken, publicUrl });
const second = verifyTwilioSignedRequest({ headers, rawBody: postBody, authToken, publicUrl });
expect(first.ok).toBe(true);
expect(first.isReplay).toBeFalsy();
expect(first.verifiedRequestKey).toBeTruthy();
expect(second.ok).toBe(true);
expect(second.isReplay).toBe(true);
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
expectReplayResultPair(first, second);
});
it("treats changed idempotency header as replay for identical signed requests", () => {
@ -355,45 +373,30 @@ describe("verifyTwilioWebhook", () => {
const postBody = "CallSid=CS778&CallStatus=completed&From=%2B15550000000";
const signature = twilioSignature({ authToken, url: urlWithQuery, postBody });
const first = verifyTwilioWebhook(
{
headers: {
host: "example.com",
"x-forwarded-proto": "https",
"x-twilio-signature": signature,
"i-twilio-idempotency-token": "idem-replay-a",
},
rawBody: postBody,
url: "http://local/voice/webhook?callId=abc",
method: "POST",
query: { callId: "abc" },
const first = verifyTwilioSignedRequest({
headers: {
host: "example.com",
"x-forwarded-proto": "https",
"x-twilio-signature": signature,
"i-twilio-idempotency-token": "idem-replay-a",
},
rawBody: postBody,
authToken,
{ publicUrl },
);
const second = verifyTwilioWebhook(
{
headers: {
host: "example.com",
"x-forwarded-proto": "https",
"x-twilio-signature": signature,
"i-twilio-idempotency-token": "idem-replay-b",
},
rawBody: postBody,
url: "http://local/voice/webhook?callId=abc",
method: "POST",
query: { callId: "abc" },
publicUrl,
});
const second = verifyTwilioSignedRequest({
headers: {
host: "example.com",
"x-forwarded-proto": "https",
"x-twilio-signature": signature,
"i-twilio-idempotency-token": "idem-replay-b",
},
rawBody: postBody,
authToken,
{ publicUrl },
);
publicUrl,
});
expect(first.ok).toBe(true);
expect(first.isReplay).toBe(false);
expect(first.verifiedRequestKey).toBeTruthy();
expect(second.ok).toBe(true);
expect(second.isReplay).toBe(true);
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
expectReplayResultPair(first, second);
});
it("rejects invalid signatures even when attacker injects forwarded host", () => {
@ -422,57 +425,22 @@ describe("verifyTwilioWebhook", () => {
});
it("accepts valid signatures for ngrok free tier on loopback when compatibility mode is enabled", () => {
const authToken = "test-auth-token";
const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000";
const webhookUrl = "https://local.ngrok-free.app/voice/webhook";
const signature = twilioSignature({
authToken,
authToken: "test-auth-token",
url: webhookUrl,
postBody,
postBody: "CallSid=CS123&CallStatus=completed&From=%2B15550000000",
});
const result = verifyTwilioWebhook(
{
headers: {
host: "127.0.0.1:3334",
"x-forwarded-proto": "https",
"x-forwarded-host": "local.ngrok-free.app",
"x-twilio-signature": signature,
},
rawBody: postBody,
url: "http://127.0.0.1:3334/voice/webhook",
method: "POST",
remoteAddress: "127.0.0.1",
},
authToken,
{ allowNgrokFreeTierLoopbackBypass: true },
);
const result = verifyTwilioNgrokLoopback(signature);
expect(result.ok).toBe(true);
expect(result.verificationUrl).toBe(webhookUrl);
});
it("does not allow invalid signatures for ngrok free tier on loopback", () => {
const authToken = "test-auth-token";
const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000";
const result = verifyTwilioWebhook(
{
headers: {
host: "127.0.0.1:3334",
"x-forwarded-proto": "https",
"x-forwarded-host": "local.ngrok-free.app",
"x-twilio-signature": "invalid",
},
rawBody: postBody,
url: "http://127.0.0.1:3334/voice/webhook",
method: "POST",
remoteAddress: "127.0.0.1",
},
authToken,
{ allowNgrokFreeTierLoopbackBypass: true },
);
const result = verifyTwilioNgrokLoopback("invalid");
expect(result.ok).toBe(false);
expect(result.reason).toMatch(/Invalid signature/);