Skip to content

Add External Phone Provider OTP-delivery Azure Function sample (JavaScript, .NET, Python) - #1

Merged
Hou (SciencePotato) merged 17 commits into
mainfrom
add-cyot-sample
Aug 21, 2026
Merged

Add External Phone Provider OTP-delivery Azure Function sample (JavaScript, .NET, Python)#1
Hou (SciencePotato) merged 17 commits into
mainfrom
add-cyot-sample

Conversation

@SciencePotato

@SciencePotato Hou (SciencePotato) commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Contributes the provider-agnostic External Phone Provider OTP-delivery Azure Function sample in three languages (Node v4, .NET 8 isolated, Python v2) against one shared contract (docs/CONTRACT.md). One dispatch engine + drop-in provider adapters (Infobip, Telesign, Soprano, Sinch); Entra JWT validation (REQUIRE_AUTH); JWE delivery context pinned to RSA-OAEP-256 + A256GCM with 5-segment/size validation; provider secrets in Key Vault via managed identity; 202 Accepted with verbatim nonce echo; fail-closed outcome-to-HTTP mapping; xUnit/node:test/pytest conformance suites. Verified end-to-end against a live Azure deployment across all three languages and all provisioned providers.

Note: 'CYOT' (Choose Your Own Telecom) remains the internal code name and is retained only in wire-level identifiers (type names, the CYOT_JWE_PRIVATE_KEY_PEM app setting, and the SAS User-Agent); customer-facing docs use 'External Phone Provider'.

See docs/ONBOARDING.md to get started.

Hou Chi Chan added 2 commits August 17, 2026 11:38
Provider-agnostic OTP-delivery Azure Function in three languages (Node v4, .NET 8 isolated, Python v2) sharing one contract (docs/CONTRACT.md): one dispatch engine + drop-in provider adapters (Infobip, Telesign, Soprano, Sinch), Entra JWT validation, JWE delivery context (RSA-OAEP-256 + A256GCM) with structural/size validation, Key Vault secrets via managed identity, 202 Accepted with verbatim nonce echo, and conformance tests.
Drop scripts/ (JWE key provisioning + nonce-echo test client) and the scripts-specific .gitignore entry.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a provider-agnostic CYOT OTP-delivery Azure Function sample implemented in JavaScript (Node.js), C# (.NET 8 isolated), and Python (Functions v2 model), all conforming to a shared contract and shipped with conformance tests and CI.

Changes:

  • Introduces a shared contract + onboarding docs and wires CI to run conformance tests in all three languages.
  • Implements dispatch engines + provider adapters (Infobip, Telesign, Soprano, Sinch) with Key Vault (managed identity) secret resolution and fail-closed outcome mapping.
  • Adds envelope parsing + JWE decryption + optional Entra JWT validation, plus language-specific HTTP triggers for POST /api/SendOtp.

Reviewed changes

Copilot reviewed 63 out of 67 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
README.md Repo entrypoint: links to implementations + contract/onboarding docs.
python/tests/test_engine.py Python engine conformance tests (dispatch pipeline + fail-closed mapping).
python/tests/test_cyot.py Python envelope + JWE round-trip + mapping tests.
python/tests/test_contract.py Python pure-contract tests (outcome/status mapping, provider request shaping).
python/src/security.py Python Entra JWT validation (REQUIRE_AUTH gating).
python/src/secrets.py Python Key Vault secret resolver w/ managed identity + short TTL cache.
python/src/registry.py Python provider adapter registry + DEFAULT_PROVIDER resolution.
python/src/providers/telesign.py Python Telesign adapter (build/parse + mapping).
python/src/providers/soprano.py Python Soprano adapter (build/parse + mapping).
python/src/providers/sinch.py Python Sinch adapter (build/parse + mapping).
python/src/providers/infobip.py Python Infobip adapter (build/parse + mapping).
python/src/providers/init.py Python providers package marker.
python/src/outcome.py Python provider-status → outcome → HTTP status mapping (fail-closed).
python/src/models.py Python shared contract models/constants (DispatchRequest + outcomes).
python/src/dispatch.py Python dispatch engine: resolve → creds → endpoint → send → map outcome/status.
python/src/cyot.py Python envelope parsing + JWE validation/decrypt + mapping to dispatch shape.
python/src/init.py Python src package marker.
python/requirements.txt Python dependency ranges for Functions runtime + crypto/JWT + Azure SDKs.
python/README.md Python implementation documentation + build/test/run/deploy instructions.
python/host.json Python Functions host config (AI sampling + extension bundle).
python/function_app.py Python HTTP trigger for SendOtp (auth/envelope/JWE/dispatch + nonce echo).
javascript/test/sendotp.test.js JS SendOtp handler tests: validation, decrypt, evaluation, auth rejection.
javascript/test/security.test.js JS privacy test: ensure OTP + phone never in logs/response body.
javascript/test/dispatch.test.js JS dispatch integration tests: providers, mapping, shutter, fail-closed.
javascript/test/auth.test.js JS token validation unit tests (REQUIRE_AUTH behavior).
javascript/src/functions/SendOtp.js JS SendOtp HTTP handler (auth/envelope/JWE/dispatch + nonce echo).
javascript/src/functions/security.js JS Entra JWT validation (JWKS + issuer/audience).
javascript/src/functions/providers/telesign.js JS Telesign adapter (manifest/build/parse).
javascript/src/functions/providers/soprano.js JS Soprano adapter (manifest/build/parse).
javascript/src/functions/providers/sinch.js JS Sinch adapter (manifest/build/parse).
javascript/src/functions/providers/infobip.js JS Infobip adapter (manifest/build/parse).
javascript/src/functions/dispatch.js JS dispatch engine: provider registry, Key Vault secrets, send + mapping.
javascript/src/functions/cyot.js JS envelope parsing + JWE validation/decrypt + mapping to dispatch shape.
javascript/README.md JS implementation documentation + configuration catalog and examples.
javascript/package.json JS dependencies + test/start scripts.
javascript/package-lock.json JS dependency lockfile for reproducible installs in CI.
javascript/host.json JS Functions host config (AI sampling + extension bundle).
dotnet/tests/EnvelopeTests.cs .NET envelope parsing + JWE round-trip tests.
dotnet/tests/EngineTests.cs .NET engine conformance tests (dispatch pipeline + privacy).
dotnet/tests/Cyot.Otp.Tests.csproj .NET test project config + references (compiles Src directly).
dotnet/tests/ContractTests.cs .NET pure-contract tests (mapping + request shaping + registry).
dotnet/Src/TokenValidator.cs .NET Entra JWT validation w/ cached OIDC config/JWKS.
dotnet/Src/SecretResolver.cs .NET Key Vault secret resolver w/ managed identity + TTL cache.
dotnet/Src/Providers/TelesignProvider.cs .NET Telesign adapter (build/parse + mapping).
dotnet/Src/Providers/SopranoProvider.cs .NET Soprano adapter (build/parse + mapping).
dotnet/Src/Providers/SinchProvider.cs .NET Sinch adapter (build/parse + mapping).
dotnet/Src/Providers/InfobipProvider.cs .NET Infobip adapter (build/parse + mapping).
dotnet/Src/ProviderRegistry.cs .NET provider registry + DEFAULT_PROVIDER resolution.
dotnet/Src/OutcomeMapper.cs .NET provider-status → outcome → HTTP status mapping (fail-closed).
dotnet/Src/Models.cs .NET shared contract records/interfaces (DispatchRequest, manifests, etc.).
dotnet/Src/ISecretResolver.cs .NET abstraction seam for secrets (testability).
dotnet/Src/IProviderAdapter.cs .NET adapter interface (manifest + build/parse).
dotnet/Src/DispatchEngine.cs .NET dispatch engine: resolve → creds → endpoint → send → map outcome/status.
dotnet/Src/DeliveryContext.cs .NET JWE decryptor + key-provider contract + Key Vault key provider.
dotnet/Src/CyotEnvelope.cs .NET envelope record + parser/validation.
dotnet/README.md .NET implementation documentation + build/test/run/deploy instructions.
dotnet/Properties/launchSettings.json .NET local launch profile.
dotnet/Program.cs .NET Functions host startup + DI registration for engine/providers.
dotnet/host.json .NET Functions host config (OpenTelemetry telemetryMode).
dotnet/Functions/SendOtp.cs .NET SendOtp HTTP trigger (auth/envelope/JWE/dispatch + nonce echo).
dotnet/dotnet.csproj .NET Functions project file + package refs.
dotnet/.gitignore .NET-specific ignore rules (local.settings.json, VS artifacts, etc.).
docs/ONBOARDING.md High-level onboarding guide (shared across languages).
docs/local.settings.sample.json Reference app settings template for local/azure configuration.
docs/CONTRACT.md Shared language-agnostic contract + required behaviors/test scenarios.
.gitignore Repo-wide ignores (local.settings.json, keys, build artifacts).
.github/workflows/ci.yml CI workflow: runs JS/node:test, .NET xUnit, Python pytest.
Files not reviewed (1)
  • javascript/package-lock.json: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/CONTRACT.md
Comment thread docs/local.settings.sample.json Outdated
Comment thread javascript/src/functions/security.js Outdated
Comment thread python/src/security.py
Comment thread python/src/dispatch.py Outdated
Hou Chi Chan added 2 commits August 18, 2026 14:44
- CONTRACT: evaluation scenario shows 202 (matches implementations)
- Sample settings: include 'python' worker runtime
- JS token validation: accept both v2 and v1 (sts.windows.net) issuers, like .NET/Python
- Python: cache PyJWKClient per tenant (avoid per-request JWKS fetch)
- Python: fall back to default when ENDPOINT_TIMEOUT_MS is non-integer
CYOT remains the internal code name and is unchanged in wire-level identifiers (type names, CYOT_JWE_PRIVATE_KEY_PEM, SAS User-Agent); CONTRACT.md now notes this explicitly.
@SciencePotato Hou (SciencePotato) changed the title Add CYOT OTP-delivery Azure Function sample (JavaScript, .NET, Python) Add External Phone Provider OTP-delivery Azure Function sample (JavaScript, .NET, Python) Aug 19, 2026
Hou Chi Chan added 13 commits August 21, 2026 09:36
…ntract

Config is now EPP_* prefixed, with a shared config module on the JavaScript side.

The endpoint returns 200 with {nonce, correlationId, providerStatus} and responds before dispatching to the provider, so it stays inside the SAS timeout budget.

JWE handling pins RSA-OAEP-256 + A256GCM, validates the five compact segments, caps body size, and accepts the base64-wrapped PEM that Key Vault references produce.

channel and mode accept both integer and lowercase-string forms; voice passcodes are digit-spaced for TTS.

Python now honours EPP_EXPECTED_ISSUER and runs delivery on a daemon thread. Removes dead code: readKid, unused manifest/secrets parameters, and the unimplemented retry interval.
Replaces the directory-scanning provider registry with four static requires. The scan swallowed load errors into a console.warn, so a typo in a provider file surfaced as an unknown-provider 400 at request time instead of failing at startup; it also did not survive bundling.

Inlines the single-line EPP_PROVIDER_ENDPOINT indirection, and logs outcome, providerStatus and providerMessageId on the background delivery so the dispatch result is no longer write-only. Corrects two comments still naming the pre-rename DEFAULT_PROVIDER setting.
…the docs

No implementation has read this setting since the retry path was dropped, so the samples were advertising a knob that does nothing.
The message logged on the preceding line already contains the passcode, so extracting it again only wrote a second copy of the secret to the log. Removes the helper from all three languages and the now-unused re import in Python.
…y Auth

EPP_EXPECTED_CLIENT_ID was only ever compared against the Easy Auth x-ms-client-principal header, and the comparison was skipped when that header was absent. None of the three token validators inspected azp or appid, so with Easy Auth disabled and EPP_REQUIRE_AUTH=true any application in the tenant holding a token for the audience was accepted.

All three validators now reject a token whose azp/appid does not match EPP_EXPECTED_CLIENT_ID. Adds five tests over the extracted isExpectedCaller decision.
config.js claimed to hold every setting the endpoint reads, but dispatch.js and security.js read their own directly. Its provider.accountName and provider.timeoutMs were read nowhere, and its DEFAULT_TIMEOUT_MS was a second competing definition of the 1500 ms default that dispatch.js already owns. Comment now describes what the module actually is.

Python discarded the dispatch body; it now logs outcome, providerStatus and providerMessageId on the [EPP] line as JavaScript does.
The envelope parsing and JWE decryption now sit at the top of dispatch.js in request order, ahead of the provider dispatch code. No test imported cyot.js directly, so the fold is confined to the module boundary.
TokenValidator, ProviderRegistry and SecretResolver called Environment.GetEnvironmentVariable directly while every other class took the injected IEnv, so they could not be exercised with a fake environment. That is why the caller check added earlier had no .NET coverage.

Extracts TokenValidator.IsExpectedCaller as a pure method, mirroring the JavaScript isExpectedCaller, and adds six tests over it and the require-auth short circuit.
… .NET, and rename Cyot to Epp

Matches the JavaScript layout: one delivery module per sample holding envelope parsing, JWE decryption and provider dispatch in request order. Removes python/src/cyot.py, dotnet/Src/CyotEnvelope.cs and dotnet/Src/DeliveryContext.cs.

Namespace Cyot.Otp becomes Epp.Otp, CyotEnvelope becomes Envelope, CyotDeliveryContext becomes DeliveryContext, and the test project is renamed. Wire-level identifiers SendCyotOtpRequest, CyotChannel, CyotDeliveryMode and CyotEndpointResponse are left alone, since those are Microsoft's and appear on the wire.
Removes eight comments that described what the following line already says, keeping the ones that carry information the code cannot: the Key Vault role requirement, the fail-closed outcome rule, and the provider failure-class mapping.
… unused JavaScript exports

models.py (18 lines), outcome.py (30) and registry.py (16) each had a single consumer and now live in dispatch.py, matching the JavaScript layout. Python src/ goes from 7 files to 4.

dispatch.js exported readProtectedHeader, loadPrivateKey, CHANNEL_BY_CODE, resolveSecretValue and DEFAULTS; none were imported anywhere, so the module's public surface shrinks from 11 symbols to 6.
It was byte-identical to test_envelope.py, so the eight envelope tests were running twice.
…mend Easy Auth on

function_app.py imported ProviderRegistry a second time from src.registry, a module that
does not exist, so the Python Function App raised ModuleNotFoundError at load and could
not start. Nothing caught it: no test imported the trigger module.

Add python/tests/test_function_app.py covering the trigger itself (route registration,
invalid JSON, nonce echo, Evaluation mode). Verified it fails on the original bug by
reintroducing the bad import.

Also:
- Remove the Key Vault warm-up (JS only; .NET and Python never had it) and take the cold
  start for now.
- Drop the channel/text arguments passed to parseResponse that no adapter reads; JS now
  matches CONTRACT.md and the .NET/Python signatures.
- Stop passing tenantId/requestProvider into dispatchOtp from SendOtp: both re-read
  EPP_PROVIDER_NAME, and .NET/Python already pass null there.
- Export resolveOutcome/outcomeToHttpStatus and assert the mapping directly instead of
  once per case through the whole pipeline, mirroring the .NET and Python contract tests.
  JS 42 -> 37 tests.
- Reduce comments to one-line rationale across all three languages.
- Document Easy Auth ON as the primary gate (the trigger is authLevel: anonymous) with
  EPP_REQUIRE_AUTH=true as the backstop, since Easy Auth is configured outside the code.
@SciencePotato
Hou (SciencePotato) merged commit 7768538 into main Aug 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants