Follow these five steps for the External Phone Provider (EPP) Function. Choose one language: JavaScript, Python, or .NET. Use CONTRACT.md for the full request contract and production limitations.
-
Purchase a provider offer from Security Store.
Open Security Store > Provider offers, purchase an offer, and activate the provider account. Confirm EPP endpoint access and support for your required channels. Obtain the provider's API key and matching customer/API ID, if required. Purchasing an offer does not deploy this Function or install a missing provider adapter.
-
Run the app setup script.
Use the guided EPP setup after manually creating only the dedicated endpoint application registration. Download only
setup/Setup-Epp.ps1; it retrieves commit-pinned support scripts, Bicep, provider profiles, and the selected language package. Choose a provider, SMS or voice, Global or EU, and a resource prefix, then approve one complete deployment plan.After approval, the script configures the app registration and enterprise application, creates the Microsoft phone-provider service principal, assigns
Epp.Invoke, grants it Microsoft GraphApplication.Read.All, and restricts the multi-tenant app through the Entra allowed-tenants preview to its home tenant plus the selected provider tenant. It then deploys the Function and configures Easy Auth. It does not purchase the provider offer, grant provider API consent/roles, or activate the EPP policy. -
Complete provider authentication and settings.
The setup script writes the selected provider route and authentication settings:
Provider API credential secret Matching identity secret Authentication telesigntelesign-api-keytelesign-customer-idBasic: base64 of customer-id:api-keysopranoNone None OAuth client assertion using the outbound user-assigned managed identity infobipinfobip-api-keyNone Authorization: App <api-key>sinchsinch-api-tokenNone Static token authentication For Telesign, store the raw key and customer ID separately and grant the Function identity Key Vault Secrets User access. For Soprano, complete provider consent/application-role onboarding for the existing multitenant application. The setup creates the disclosed federated identity credential; it does not grant access to Soprano's API.
Start with local.settings.sample.json beside the chosen app's
host.json. Replace placeholders inValues; all values must be strings.EPP_PROVIDER_ENDPOINTis the complete provider-approved request URL selected for the channel and Global/EU region.EPP_PROVIDER_AUTH_MODEmust match the adapter:apiKeyfor Telesign oroauthfor Soprano. Provider API keys stay in Key Vault, notValues.Reuse the values from the setup-created Function App's environment variables; do not create a second set of application or identity IDs for local settings. The shared sample uses these same names for JavaScript, Python, and .NET:
Local ValueskeysValue to reuse from setup EPP_PROVIDER_NAME,EPP_PROVIDER_AUTH_MODESelected provider and its authentication mode. EPP_PROVIDER_ENDPOINT,EPP_PROVIDER_CHANNEL,EPP_PROVIDER_ENDPOINT_REGIONComplete selected send URL, smsorvoice, andglobaloreu. Do not append an API path.EPP_PROVIDER_TENANT_IDProvider tenant, not the customer's home tenant. EPP_PROVIDER_APP_ID,EPP_PROVIDER_SCOPESoprano API application ID and the exact selected scope, including /.default. These are not the calling application's ID. Leave blank for API-key providers.EPP_OUTBOUND_CLIENT_IDExisting calling application's Application (client) ID used during setup, not its Object ID or Soprano's API ID. EPP_OUTBOUND_MI_CLIENT_IDSetup-created outbound user-assigned identity's Client ID, not its principal/Object ID. EPP_PROVIDER_TIMEOUT_MS,EPP_PROVIDER_RETRY_INTERVAL_MSSelected profile values, as strings. The retry interval does not enable runtime retries or shutter mode. KEY_VAULT_URLSetup-created or explicitly selected credential vault URL, not a secret value. The outbound IDs are used only for Soprano OAuth; leave them blank for local API-key-only configurations.
EPP_PROVIDER_APP_IDandEPP_PROVIDER_ENDPOINT_REGIONare setup metadata; the runtime uses the selected scope and full URL directly. Keep Azure host storage, package URLs, Application Insights, and Easy Auth configuration in Azure rather than copying the complete cloud environment into a local settings file. Use the local storage and key guidance below.Migrating local settings from the earlier optional-JWT branch: replace
EPP_PROVIDER_APPLICATION_IDwithEPP_OUTBOUND_CLIENT_IDandEPP_PROVIDER_MI_CLIENT_IDwithEPP_OUTBOUND_MI_CLIENT_ID. ReuseEPP_PROVIDER_TENANT_IDand the setup-selectedEPP_PROVIDER_SCOPE. RemoveEPP_PROVIDER_JWT_ENABLED; currentmainusesEPP_PROVIDER_NAME=sopranowithEPP_PROVIDER_AUTH_MODE=oauth, not an optional JWT flag. Replace the old base endpoint with the complete send URL.AZURE_CLIENT_ID, if separately configured for Key Vault, is not a substitute forEPP_OUTBOUND_MI_CLIENT_ID.Set
FUNCTIONS_WORKER_RUNTIMEtonode,python, ordotnet-isolated. LocalUseDevelopmentStorage=truerequires Azurite; configure Azure host storage separately. Use a local test private key forEPP_DECRYPTION_KEY_PEM; in Azure, use a Key Vault reference and give the caller the matching public key. Core Tools does not resolve Key Vault references locally.EPP_ENCRYPTION_KEY_IDis advisory only; this sample has one decryption key, not multi-key rotation. The decryption key, provider credentials, and caller authentication are separate.For local work, keep the host loopback-only, without tunnels or public forwarding. Core Tools has no Easy Auth, and managed identity cannot use your CLI login. Use offline tests or evaluation mode by default; do not add a production credential fallback merely to test locally.
-
Deploy the Functions.
Configure App Service Authentication (Easy Auth) before exposing the endpoint. It is the only caller-authentication gate; the Function handler is anonymous and has no backup validator. Never use
-NoEasyAuth, trust forwarded principal headers, or treat JWE/nonce proof as caller authentication. Anyone with the public encryption key can create a JWE request.Easy Auth setting Required value globalValidation.requireAuthenticationtrueglobalValidation.unauthenticatedClientActionReturn401httpSettings.requireHttpstrueidentityProviders.azureActiveDirectory.registration.clientIdEndpoint app's client ID identityProviders.azureActiveDirectory.registration.openIdIssuerTrusted tenant issuer matching the caller's token version; never commonororganizationsidentityProviders.azureActiveDirectory.validation.allowedAudiencesExact endpoint-app audience agreed with SAS identityProviders.azureActiveDirectory.validation.defaultAuthorizationPolicy.allowedApplicationsNonempty allowlist of authorized SAS caller application IDs, not the endpoint app ID globalValidation.excludedPathsNo exemption for /api/SendOtpor an alternate ingress routeFor v1 tokens, use the agreed Application ID URI audience and
https://sts.windows.net/{tenantId}/issuer; for v2, use the matching v2 issuer and agreed audience, normally the endpoint app client-ID GUID. Do not derive trust from requesttenantId. Key Vault RBAC for the Function identity is separate from authorizing SAS callers; this code does not check an Entra application role.Leave the endpoint app registration's
tokenEncryptionKeyIdnull: encrypted Entra access tokens are not supported. If correcting an existing registration, do not delete certificates or change signing keys; request a fresh token afterward. This does not disable the required JWE encryption ofencryptedDeliveryContextor removeEPP_DECRYPTION_KEY_PEM.Download the selected language's Function ZIP from GitHub Releases, or use the root-level packaging scripts for custom builds. The .NET source ZIP must be extracted and built/published with the .NET 8 SDK or a build-enabled deployment pipeline. The Python source ZIP requires Azure remote build on Linux. Neither source ZIP is ready for direct run-from-package. Downloading or building a ZIP does not deploy it.
Build and publish only the selected language folder with its runtime dependencies, not the repository root or stale output. Inspect the package: exclude local settings, private keys, credentials, tests, and diagnostic scripts using the runtime's
.funcignoreand publish rules. Apply the settings from step 3 to the Function App's Azure environment; local settings are not published automatically. Verify managed identity access and keep public ingress disabled until authentication is configured. Source changes do not update an existing deployment.The root .gitignore covers all runtimes; publishing uses separate exclusions in JavaScript, Python, and .NET. The .NET project also excludes local settings from publish output.
-
Validate.
Send an authorized
POST /api/SendOtpwith a valid encrypted envelope andmode: 2ormode: "evaluation". Expect200and the matching nonce, with no provider lookup, provider secret reads, or outbound provider HTTP. See the evaluation contract.Before live testing, verify these cases on the deployed endpoint, not just in offline tests:
Check Expected result Missing, malformed, expired, or invalidly signed caller token 401before the handlerWrong issuer/audience or caller outside the allowlist Rejected before the handler Authorized caller and valid evaluation envelope 200with matching nonce, no provider I/OAlternate routes, hostnames, and serving slots No authentication or HTTPS bypass After those checks pass, confirm the destination and channel, use
mode: 1, and submit once.200with a matching nonce confirms provider acceptance, not handset delivery. Confirm receipt and spoken digit clarity through the handset/provider reports. The message is forwarded unchanged. Do not blindly retry a timeout: the provider may already have accepted the request. Review the production limitations, including no durable handoff, early acknowledgement, expiry enforcement, or multi-key rotation.Never record phone numbers, messages, nonce values, tokens, API keys, encrypted request bodies, or raw provider responses in reports. Keep platform/SDK body tracing off. Repeat the deployed checks after deployment, authentication changes, and slot swaps. Local evaluation and passing unit tests do not certify platform authentication or live delivery.