Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ on:
pull_request:

jobs:
epp-setup:
name: EPP setup (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Compile Bicep without deploying
shell: pwsh
run: |
az bicep install
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
az bicep build --file (Join-Path $env:GITHUB_WORKSPACE 'setup/infra/main.bicep') --outfile (Join-Path $env:RUNNER_TEMP 'epp-main.json')
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

javascript:
name: JavaScript (Node.js)
runs-on: ubuntu-latest
Expand Down
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,25 @@ by default. Deploy each language separately, not all three to the same Function
New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** for setup, config, running, securing,
and deploying, step by step.

## Guided EPP setup

Use **[setup](setup/docs/README.md)** for **Step 2: endpoint deployment**. Download only
`Setup-Epp.ps1`; it downloads its supporting PowerShell, Bicep, package catalog, and provider JSON
from the same commit. Customers select a language, provider, SMS or voice, Global or EU endpoint,
and a resource prefix, then approve one complete plan. Manual Step 1 only creates the dedicated app
registration; PowerShell configures its service principals, `Epp.Invoke`, Microsoft caller access,
Graph `Application.Read.All`, the provider-tenant allowlist preview, encryption certificate, and
Easy Auth. The home tenant remains allowed by Entra. Policy activation remains manual.

## Download a Function ZIP

Download the preview ZIP for your chosen language:

| Language | Download | Contents |
|---|---|---|
| JavaScript | [epp-javascript.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-packages-preview-20260914/epp-javascript.zip) | Application and production dependencies |
| .NET | [epp-dotnet-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-dotnet-source-preview-20260915/epp-dotnet-source.zip) | C# Function source and project file; build/publish before deployment |
| Python | [epp-python-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-packages-preview-20260914/epp-python-source.zip) | Source for Azure remote build on Linux |
| JavaScript | [epp-javascript.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-guided-setup-preview-20260915/epp-javascript.zip) | Application and production dependencies |
| .NET | [epp-dotnet-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-guided-setup-preview-20260915/epp-dotnet-source.zip) | C# Function source and project file; build/publish before deployment |
| Python | [epp-python-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-guided-setup-preview-20260915/epp-python-source.zip) | Source for Azure remote build on Linux |

Customers do not need PowerShell or a local build toolchain to download these files. Verify downloads
against the corresponding release's `SHA256SUMS.txt`. Configure the target Function App's runtime, app settings,
Expand Down Expand Up @@ -141,7 +151,12 @@ how code accesses configuration, not the environment-variable names.
| `EPP_DECRYPTION_KEY_PEM` | Every request | Local test PEM or base64 PEM. In Azure, use a Key Vault reference resolving to the private-key secret. |
| `EPP_ENCRYPTION_KEY_ID` | Optional | Expected encryption key ID; mismatch only produces an advisory warning. |
| `EPP_PROVIDER_NAME` | Live delivery | Selected adapter's manifest ID. No default provider. |
| `EPP_PROVIDER_ENDPOINT` | Live delivery | HTTPS **base URL**, in the same environment as the provider credentials; the adapter adds its route. |
| `EPP_PROVIDER_ENDPOINT` | Live delivery | Complete provider-approved HTTPS request URL selected from the provider profile. |
| `EPP_PROVIDER_CHANNEL` | Guided deployment | Selected `sms` or `voice` route; other live-request channels fail closed. |
| `EPP_PROVIDER_ENDPOINT_REGION` | Guided deployment metadata | Selected `global` or `eu` route label. |
| `EPP_PROVIDER_AUTH_MODE` | Live delivery | Must match the adapter: `apiKey` for Telesign or `oauth` for Soprano. |
| `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE` | Soprano OAuth | Provider tenant and selected API scope. |
| `EPP_OUTBOUND_CLIENT_ID`, `EPP_OUTBOUND_MI_CLIENT_ID` | Soprano OAuth | Existing multitenant application and outbound user-assigned managed identity used for client-assertion exchange. |
| `EPP_PROVIDER_TIMEOUT_MS` | Optional | Decimal milliseconds. Defaults to `1500`, capped at `2500`; not an end-to-end deadline. |
| `EPP_PROVIDER_ACCOUNT_NAME` | Adapter-dependent | Sender/account metadata, not an API key or credential identity. |
| `KEY_VAULT_URL` | Provider credential lookup | URI of the vault containing the manifest-named provider secrets. Separate from the encryption-key reference. |
Expand All @@ -153,11 +168,9 @@ how code accesses configuration, not the environment-variable names.
2. **In Azure:** set the same application variables on the selected Function App (or serving slot)
under **Settings → Environment variables → App settings**, then apply the changes. Local settings
are not published automatically. Configure host storage separately for the selected hosting plan.
3. Store provider API keys and any required identity secrets in Key Vault using the **exact names in
the adapter manifest**. Grant that app/slot's managed identity *Key Vault Secrets User* on those
secrets. An API key in a local environment variable is not a supported replacement for the resolver.
See the [provider credential naming table](docs/ONBOARDING.md#provider-credential-names) and
[local use of existing cloud secrets](docs/ONBOARDING.md#local-settings-and-cloud-secrets).
3. For Telesign, store provider API credentials in Key Vault using the exact manifest names. For
Soprano, configure provider consent plus the profile's tenant/scope and outbound managed-identity
federation; the Function stores no Soprano client secret.

Evaluation requests do not need provider variables or provider secrets. They still need the decryption
key. The default credential resolvers use `ManagedIdentityCredential`, **not** the developer's CLI
Expand All @@ -170,15 +183,13 @@ or base64 PEM directly; use a reference such as `@Microsoft.KeyVault(SecretUri=h
for `EPP_DECRYPTION_KEY_PEM` in Azure app settings, where the platform resolves it.

Configure inbound issuer/audience/caller trust in **Easy Auth**, not these application variables.
Incoming `tenantId`, `channel`, `mode` and `ttlSeconds` are request data. No outbound OAuth settings
are supported by this main-based implementation.
Incoming `tenantId`, `channel`, `mode` and `ttlSeconds` are request data and never override the
configured provider route or authentication.

## Telesign EPP

The `telesign` adapter uses `POST https://verify.telesign.com/integration/msft/cyot`
for both SMS and Voice. Set `EPP_PROVIDER_NAME=telesign` and
`EPP_PROVIDER_ENDPOINT=https://verify.telesign.com` (the base URL, without the route).
This replaces the legacy `/v1/messaging` and `/v1/voice` integrations in all three languages.
The `telesign` adapter sends its JSON contract to the complete SMS or voice URL selected from the
provider profile. It does not append or infer a route.

Basic authentication uses `base64(customer-id:api-key)`, with the existing Key Vault secrets
`telesign-customer-id` and `telesign-api-key`. Digest and Phase 2 token authentication are not
Expand Down Expand Up @@ -212,7 +223,7 @@ lookup entirely, rather than invoking Telesign shutter mode.

Responses normalize `reference_id` and `status.code`/`status.description` internally; provider
metadata is not logged or exposed in the public nonce response. Existing numeric success codes
are retained (SMS: 200, 203, 290-292; Voice: 100-103). CYOT code `3001` ("Message in progress"),
are retained (SMS: 200, 203, 290-292; Voice: 100-103). EPP code `3001` ("Message in progress"),
observed for both channels, is also accepted on successful HTTP responses. This acknowledges
provider acceptance, not handset receipt or completed audio playback. The supplied EPP integration
overview does not provide a complete replacement status-code catalog. Missing, malformed, or
Expand Down
49 changes: 27 additions & 22 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ nonblank strings. Supply the password explicitly to preserve leading zeros; it i
from `message`. These values are forwarded unchanged as `voice.text2voice`, without a top-level
`text` field. Missing or invalid speech returns `400` before credential lookup or provider HTTP.
SMS continues to use `message`, and evaluation continues to skip provider-specific validation and I/O.
Soprano authentication remains API-key-only (`X-MEMS-API-ID` and `X-MEMS-API-Key`, resolved from
`soprano-api-id` and `soprano-api-key` in Key Vault). No provider JWT, OAuth flow, token endpoint,
or bearer-token forwarding is added. Existing platform caller authentication is unchanged.
Soprano uses OAuth client-assertion exchange. The outbound user-assigned managed identity obtains an
`api://AzureADTokenExchange/.default` assertion for the existing multitenant application, which then
requests the configured provider scope. Existing platform caller authentication is unchanged.

Use a speech language supported by the selected Soprano endpoint and account. On QA4, an API-key
Use a speech language supported by the selected Soprano endpoint and account. On QA4, an OAuth
voice request using `en` returned HTTP `400` with error code `400101`; the same request structure
using `en-US` returned HTTP `201` with `ENROUTE` on September 15, 2026. This confirms acceptance,
not handset receipt or audio quality. The adapter preserves the supplied language and does not
Expand Down Expand Up @@ -183,8 +183,9 @@ success-looking status. Explicit `Block`/`StepUp` outcomes remain non-success re
Each provider is one unit exposing three things:

- **`manifest`**: protocol facts only:
- `id`: provider id selected by `EPP_PROVIDER_NAME`; its base URL is `EPP_PROVIDER_ENDPOINT`
- `auth`: `{ mode: 'apiKey', keyVaultSecretName, identityKeyVaultSecretName? }`; other modes fail closed
- `id`: provider id selected by `EPP_PROVIDER_NAME`; its complete request URL is `EPP_PROVIDER_ENDPOINT`
- `auth`: either `{ mode: 'apiKey', keyVaultSecretName, identityKeyVaultSecretName? }` or
`{ mode: 'oauth' }`; unsupported modes fail closed
- `responseMapping`: map of provider status → `Continue` | `Fail` | `Block` | `StepUp` (+ `default`)
- **`buildRequest({ channel, endpoint, dispatch, credential, env })`** → `{ url, method, headers, body }`
- **`parseResponse({ httpStatus, ok, json })`** → `ParsedResponse`, containing `success`,
Expand All @@ -205,19 +206,17 @@ is parsed once and normalized inside its adapter. No serialization framework or
class hierarchy is required.

Adapters require registration in the chosen runtime. Consult the selected adapter and its manifest
for required credentials and options: the manifest declares secret names and protocol mappings;
for required credentials and options: the manifest declares authentication and protocol mappings;
the implementation reads adapter-specific options from app settings. Individual API contracts remain
in the adapters; the [onboarding credential naming table](ONBOARDING.md#provider-credential-names)
lists the exact manifest secret names for provisioning and authorized local tests. Keep that table
aligned with the manifests; never include secret values in documentation or the settings sample.

### Telesign CYOT integration
### Telesign EPP integration

SMS and Voice both use `POST https://verify.telesign.com/integration/msft/cyot` with JSON. Configure
the base URL as `https://verify.telesign.com`. The adapter supplies `recipient.phone_number`, the
unchanged `message.text`, optional `message.language`, one selected `channels[].channel`, and
`correlation_id`. Keep the leading `+` in the E.164 phone number; the guide's example `12345678`
does not satisfy its own required phone-number pattern.
SMS and Voice use the complete provider-approved URLs selected from the provider profile. The adapter
supplies `recipient.phone_number`, the unchanged `message.text`, optional `message.language`, one
selected `channels[].channel`, and `correlation_id`. Keep the leading `+` in the E.164 phone number.

Phase 1 supports Basic and Digest; this sample implements Basic only. Per
[Telesign's authentication instructions](https://developer.telesign.com/enterprise/docs/authentication#basic-authentication),
Expand Down Expand Up @@ -246,18 +245,23 @@ Set by provisioning. **Identical names across all languages.**
| Key | Purpose |
|-----|---------|
| `EPP_PROVIDER_NAME` | registered id of the selected provider; `<adapter-id>` is a placeholder, not a bundled default |
| `EPP_PROVIDER_ENDPOINT` | absolute HTTPS base URL with a hostname, port 1–65535, and no userinfo or fragment; the final adapter URL is also validated; redirects are not followed |
| `EPP_PROVIDER_ENDPOINT` | complete absolute HTTPS request URL for the selected channel/region, with a hostname, port 1–65535, and no userinfo or fragment; redirects are not followed |
| `EPP_PROVIDER_CHANNEL` | optional configured `sms` or `voice` route; when set, other live-request channels fail closed |
| `EPP_PROVIDER_ENDPOINT_REGION` | selected `global` or `eu` route label; informational at runtime |
| `EPP_PROVIDER_AUTH_MODE` | must match the selected adapter (`apiKey` for Telesign, `oauth` for Soprano) |
| `EPP_PROVIDER_TENANT_ID` | selected provider tenant; added to the Step 1 app's allowed-tenants preview and used as the OAuth authority for Soprano |
| `EPP_PROVIDER_SCOPE` | Soprano OAuth scope |
| `EPP_OUTBOUND_CLIENT_ID`, `EPP_OUTBOUND_MI_CLIENT_ID` | client application and user-assigned identity used for Soprano client-assertion exchange |
| `EPP_PROVIDER_ACCOUNT_NAME` | sender/source only when required by the selected adapter |
| `EPP_PROVIDER_TIMEOUT_MS` | trimmed ASCII decimal milliseconds; default 1500 for missing/invalid/nonpositive values; capped at 2500. Not a whole-invocation deadline |
| `EPP_DECRYPTION_KEY_PEM` | single RSA private key for JWE decryption, PEM or base64-encoded PEM; use a Key Vault secret reference in Azure, not a plaintext private key in shared settings |
| `EPP_ENCRYPTION_KEY_ID` | optional expected JWE `kid`; after successful decryption, a mismatch emits only `encryption_key_id_mismatch`. Advisory, not a key selector or authentication check |
| `KEY_VAULT_URL` | Key Vault URI (provider API keys) |
| `KEY_VAULT_URL` | Key Vault URI for API-key providers |
| `AZURE_CLIENT_ID` | set for a user-assigned managed identity |

Provider credential values live in **Key Vault**, under the names in the selected adapter's manifest,
and are fetched via **managed identity** with the *Key Vault Secrets User* role. Do not put credential
values in code or app settings. No additional customer-private configuration or new environment
variable is needed for this guidance.
Telesign credentials live in **Key Vault**, under the names in its manifest, and are fetched via
managed identity. Soprano exchanges an outbound managed-identity assertion for a token in the
configured provider tenant/scope. Do not put provider secrets in code or app settings.

Caller trust is configured in **Easy Auth**, not application environment variables: pin the trusted
tenant issuer, the endpoint-app audience and the authorized SAS caller application ID. Incoming
Expand All @@ -266,9 +270,10 @@ guard or backup token validation. See [platform onboarding](ONBOARDING.md#2-prov

### Default provider and configuration readers

Provision `EPP_PROVIDER_NAME` with the customer's selected provider, plus that account's
`EPP_PROVIDER_ENDPOINT` and Key Vault credentials. A missing or unknown provider fails closed;
there is no implicit default or automatic failover. Request-body provider fields are not used.
Provision `EPP_PROVIDER_NAME` with the customer's selected provider, plus the complete selected
channel/region `EPP_PROVIDER_ENDPOINT` and matching authentication settings. A missing or unknown
provider fails closed; there is no implicit default or automatic failover. Request-body provider
fields are not used.

The shared configuration readers are [JavaScript `readConfig`](../javascript/src/functions/config.js),
[Python `read_config`](../python/src/config.py), and [.NET `AppConfig.Read`](../dotnet/Src/AppConfig.cs).
Expand Down
Loading
Loading