feat: verify telnyx webhook signature optionally

This commit is contained in:
Sabiha Khan 2026-05-12 19:45:35 +05:30
parent 137b5e9f89
commit babe249150
4 changed files with 53 additions and 8 deletions

View file

@ -142,3 +142,12 @@ FORCE_TURN_RELAY = os.getenv("FORCE_TURN_RELAY", "false").lower() == "true"
# OSS Email/Password Auth
OSS_JWT_SECRET = os.getenv("OSS_JWT_SECRET", "change-me-in-production")
OSS_JWT_EXPIRY_HOURS = int(os.getenv("OSS_JWT_EXPIRY_HOURS", "720")) # 30 days
# REMOVE-AFTER 2026-05-15: transitional flag. When True, Telnyx webhook
# signature verification is skipped for configs that have no
# webhook_public_key set (existing configs predating the field). Set in prod
# through 2026-05-15 to give users time to add their key; once removed,
# configs without a key will fail signature verification.
TELNYX_WEBHOOK_VERIFICATION_OPTIONAL = (
os.getenv("TELNYX_WEBHOOK_VERIFICATION_OPTIONAL", "false").lower() == "true"
)

View file

@ -25,6 +25,7 @@ TELNYX_TIMESTAMP_TOLERANCE_SECONDS = 300
TELNYX_PUBLIC_KEY_BYTES = 32
TELNYX_SIGNATURE_BYTES = 64
from api.constants import TELNYX_WEBHOOK_VERIFICATION_OPTIONAL
from api.enums import WorkflowRunMode
from api.services.telephony.base import (
CallInitiationResult,
@ -210,6 +211,12 @@ class TelnyxProvider(TelephonyProvider):
return False
if not self.webhook_public_key:
# REMOVE-AFTER 2026-05-15: transition window. Allow webhooks
# through for configs that haven't added the key yet. Remove this
# branch along with TELNYX_WEBHOOK_VERIFICATION_OPTIONAL after
# the cutoff.
if TELNYX_WEBHOOK_VERIFICATION_OPTIONAL:
return True
logger.error("Missing Telnyx webhook_public_key configuration")
return False

View file

@ -153,16 +153,45 @@ async def test_verify_inbound_signature_rejects_missing_config_public_key():
_, headers = _signed_headers(body)
provider = _provider()
result = await provider.verify_inbound_signature(
"https://example.test/api/v1/telephony/inbound/run",
json.loads(body),
headers,
body,
)
# REMOVE-AFTER 2026-05-15: drop the patch wrapper once
# TELNYX_WEBHOOK_VERIFICATION_OPTIONAL is removed; the bare call below
# will then assert the only path.
with patch(
"api.services.telephony.providers.telnyx.provider.TELNYX_WEBHOOK_VERIFICATION_OPTIONAL",
False,
):
result = await provider.verify_inbound_signature(
"https://example.test/api/v1/telephony/inbound/run",
json.loads(body),
headers,
body,
)
assert result is False
# REMOVE-AFTER 2026-05-15: delete this whole test along with the
# TELNYX_WEBHOOK_VERIFICATION_OPTIONAL flag.
@pytest.mark.asyncio
async def test_verify_inbound_signature_allows_missing_key_when_optional_flag_set():
body = _body()
_, headers = _signed_headers(body)
provider = _provider()
with patch(
"api.services.telephony.providers.telnyx.provider.TELNYX_WEBHOOK_VERIFICATION_OPTIONAL",
True,
):
result = await provider.verify_inbound_signature(
"https://example.test/api/v1/telephony/inbound/run",
json.loads(body),
headers,
body,
)
assert result is True
@pytest.mark.asyncio
async def test_verify_inbound_signature_reads_headers_case_insensitively():
body = _body()

4
ui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "ui",
"version": "1.27.0",
"version": "1.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "1.27.0",
"version": "1.28.0",
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@nangohq/frontend": "^0.69.47",