From b670004725c839408e8a2c89d497e69182d7f079 Mon Sep 17 00:00:00 2001 From: Sabiha Khan <87858386+chewwbaka@users.noreply.github.com> Date: Tue, 12 May 2026 19:47:28 +0530 Subject: [PATCH] feat: verify telnyx webhook signature optionally (#279) --- api/constants.py | 9 ++++ .../telephony/providers/telnyx/provider.py | 7 ++++ api/tests/telephony/telnyx/test_provider.py | 41 ++++++++++++++++--- ui/package-lock.json | 4 +- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/api/constants.py b/api/constants.py index f44410c..a98612f 100644 --- a/api/constants.py +++ b/api/constants.py @@ -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" +) diff --git a/api/services/telephony/providers/telnyx/provider.py b/api/services/telephony/providers/telnyx/provider.py index f14e0f1..e78914b 100644 --- a/api/services/telephony/providers/telnyx/provider.py +++ b/api/services/telephony/providers/telnyx/provider.py @@ -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 diff --git a/api/tests/telephony/telnyx/test_provider.py b/api/tests/telephony/telnyx/test_provider.py index 6462119..3f0f6f3 100644 --- a/api/tests/telephony/telnyx/test_provider.py +++ b/api/tests/telephony/telnyx/test_provider.py @@ -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() diff --git a/ui/package-lock.json b/ui/package-lock.json index 2edb193..db13e2f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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",