Skip to content
Open
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
41 changes: 30 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,29 @@ by default. Deploy each language separately, not all three to the same Function
New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — 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 tools, Bicep, and provider JSON from GitHub. Supply
missing customer settings, select **JavaScript, .NET, or Python**, choose Telesign or Soprano,
**SMS or voice**, **Global or EU**, enter a resource prefix, and approve one complete resource plan.
Generated names add `epp` after the customer prefix. Missing required Azure resource providers
are registered automatically after approval. Package links and published checksums are
selected automatically. Setup publishes .NET for Linux and requests Azure remote build for Python;
customers do not build or deploy the source ZIPs manually. The .NET choice requires the .NET 8 SDK.
Application registration (Step 1) and policy activation (Step 3) remain manual. Missing provider
details are explicitly labelled test values and written to the real Function App settings; replace
them and complete Telesign API-key or Soprano OAuth onboarding before live delivery.

## 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/siyixian/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-provider-auth-preview-20260915/epp-javascript.zip) | Application and production dependencies |
| .NET | [epp-dotnet-source.zip](https://github.com/siyixian/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-provider-auth-preview-20260915/epp-dotnet-source.zip) | C# Function source and project file; build/publish before deployment |
| Python | [epp-python-source.zip](https://github.com/siyixian/ExternalPhoneProvider-AzureFunction-Sample/releases/download/epp-provider-auth-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 All @@ -42,14 +56,14 @@ project; Python requires remote build to install dependencies. Neither source ZI
as a run-from-package artifact. GitHub's **Code > Download ZIP**
is the whole source repository, not a Function deployment package.

After the packaging workflow is merged, each successful `main` build tests all three implementations,
The private test links above match `test/epp-single-script`. After the packaging workflow is merged
upstream, each successful `main` build tests all three implementations,
builds and inspects the ZIPs, and publishes a new versioned release. Get those builds from
[Latest release](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest).
Older releases remain available; existing assets are not overwritten. Pull requests build downloadable
workflow artifacts only and cannot publish releases. GitHub sign-in may be required for workflow
artifacts, but public release downloads do not require a local build. Packaging does not deploy or
verify live provider delivery. The current preview is built from the packaging branch, not a merged
release of the separate provider feature branches.
verify live provider delivery.

## Build ZIPs Locally

Expand Down Expand Up @@ -106,7 +120,7 @@ extend it deliberately if you add runtime assets, and never put secrets in appli
## The design in one line

SAS → Easy Auth → anonymous HTTP handler (`POST /api/SendOtp`, validate envelope + decrypt JWE) →
configured provider (API key) → HTTP result with nonce on success.
configured provider (Telesign API key or Soprano OAuth) → HTTP result with nonce on success.
Only provider acceptance returns the nonce for live requests. Incoming `mode: 2` (evaluation) is the
generic shutter: after platform authentication, validate and decrypt, then echo the nonce without
calling a provider.
Expand Down Expand Up @@ -141,7 +155,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 for the selected channel and endpoint region. |
| `EPP_PROVIDER_CHANNEL` | Guided deployment | Selected `sms` or `voice` route; live requests for the other channel 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 client ID and outbound user-assigned managed identity client ID 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,9 +172,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.
3. For Telesign, store provider API credentials in Key Vault using the **exact names in the adapter
manifest** and grant the Function identity *Key Vault Secrets User*. For Soprano, configure the
provider tenant/scope and outbound managed-identity federation; no provider secret is stored.

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 Down
22 changes: 15 additions & 7 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,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 Down Expand Up @@ -186,12 +187,17 @@ 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`, `EPP_PROVIDER_SCOPE` | Soprano provider tenant and 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,
Expand All @@ -206,9 +212,11 @@ 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. Telesign resolves its
API-key credentials from Key Vault. Soprano exchanges an outbound managed-identity assertion for a
token in the configured provider tenant/scope. 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
52 changes: 30 additions & 22 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,36 @@ define required credentials and options. No provider is preferred or selected by

## 1. Select and configure an adapter

Choose a registered adapter for the selected provider and an account supporting the required channels.
Set `EPP_PROVIDER_NAME` to its actual manifest id (`<adapter-id>` is only a placeholder), and configure
its matching `EPP_PROVIDER_ENDPOINT` and required options. One provider is active per deployment;
request fields cannot change it. Purchasing or activating a subscription does not install an adapter.
Choose a registered adapter for the selected provider, channel, and endpoint region. Set
`EPP_PROVIDER_NAME` to its actual manifest id (`<adapter-id>` is only a placeholder), and configure
the complete selected request URL in `EPP_PROVIDER_ENDPOINT`. One provider and channel route are
active per guided deployment; request fields cannot change them.

Store credentials under the Key Vault secret names declared by the selected adapter's manifest, not
in code or app settings. Grant the Function's managed identity *Key Vault Secrets User* access at the
appropriate secret or vault scope. Confirm that the endpoint and credentials belong to the same
account and environment. Individual API contracts stay in the adapters.

### Setup script compatibility
### Guided setup compatibility

The Preview 1 setup script creates the encryption-key secret, not the selected provider's API
credentials. Before live delivery, complete these steps:
The [EPP Step 2 setup](../setup/docs/README.md) uses one downloadable launcher, GitHub-hosted
language/provider catalogs, and one Bicep deployment approval. It downloads the selected language
ZIP, verifies its published checksum automatically, builds .NET for Linux or requests Azure remote
build for Python, and deploys the ready-to-run result. Application registration and policy activation
are manual. Authentication is provider-owned: Telesign uses API keys; Soprano uses OAuth
client-assertion exchange and creates the disclosed outbound federated identity credential.

Before live delivery, complete these steps:

1. Set `KEY_VAULT_URL` to the vault containing the provider credentials. When it is the vault created
by setup, use that vault's `vaultUri`; otherwise explicitly select the credential vault and grant
the Function identity read access there. An encryption-key reference does not configure this client.
2. Store the API key under the selected manifest's `keyVaultSecretName` (`key_vault_secret_name` in
Python). If the manifest also declares `identityKeyVaultSecretName`
(`identity_key_vault_secret_name`), store the matching API/customer ID as a separate secret.
`EPP_PROVIDER_ACCOUNT_NAME` is a sender/account option, **not** that credential ID or the API key.
Keep secret values out of parameters, console transcripts and checked-in settings.
3. Give `EPP_PROVIDER_ENDPOINT` the **base URL expected by the adapter**. Bundled adapters append the
channel-specific API path. Do not pass an already complete send URL unless an adapter explicitly
expects it. Use the same account/environment for the endpoint and its credential pair.
2. For Telesign, store the API key and customer ID under the manifest's exact secret names.
`EPP_PROVIDER_ACCOUNT_NAME` is a sender/account option, **not** either credential. For Soprano,
complete provider consent/application-role onboarding for the existing multitenant application;
the Function stores no Soprano client secret.
3. Give `EPP_PROVIDER_ENDPOINT` the complete provider-approved URL for the selected channel and
Global/EU region. The Telesign and Soprano adapters use it exactly and do not append a route.
4. Supply any additional options read by the selected adapter. Registering a provider does not make
every account option or channel automatically available.

Expand All @@ -41,16 +45,20 @@ The script already writes the correct `EPP_` names; no variable-prefix translati
| Setup value | Current application behavior |
|---|---|
| `EPP_PROVIDER_NAME` | Selects one registered adapter; no implicit default. |
| `EPP_PROVIDER_ENDPOINT` | Base URL, with the final send path built by the adapter. |
| `EPP_PROVIDER_ENDPOINT` | Complete selected provider request URL. |
| `EPP_PROVIDER_CHANNEL` | Restricts live delivery to the selected `sms` or `voice` route. |
| `EPP_PROVIDER_ENDPOINT_REGION` | Records the selected `global` or `eu` route. |
| `EPP_PROVIDER_AUTH_MODE` | `apiKey` for Telesign; `oauth` for Soprano. |
| `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE` | Soprano OAuth target tenant and scope. |
| `EPP_PROVIDER_TIMEOUT_MS` | Default 1500 ms; positive decimal values are capped at 2500 ms. Zero/invalid values use the default, not an infinite timeout. |
| `EPP_PROVIDER_RETRY_INTERVAL_MS` | Not consumed. Calls are not automatically retried; writing this setting does not enable retries. |
| `EPP_PROVIDER_ACCOUNT_NAME` | Adapter-specific sender/account option, separate from credential secrets. |
| `EPP_DECRYPTION_KEY_PEM` | PEM or base64 PEM, usually resolved from a Key Vault secret reference. |
| `EPP_ENCRYPTION_KEY_ID` | Advisory mismatch warning only; not overlapping-key selection. |
| `EPP_EXPECTED_AUDIENCE`, `EPP_EXPECTED_ISSUER`, `EPP_EXPECTED_CLIENT_ID`, `EPP_TENANT_ID` | The script may write these, but this platform-authenticated application does not read them. The script's separate Easy Auth configuration enforces caller trust. |

**Do not use the script's `-NoEasyAuth` option with this application.** There is no application token
validator to take over. For the script's v1 registration, configure Easy Auth with the identifier URI
**Do not disable Easy Auth with this application.** There is no application token
validator to take over. For a v1 registration, configure Easy Auth with the identifier URI
as audience, `https://sts.windows.net/{tenantId}/` as issuer, and the authorized SAS application in
`allowedApplications`. Use the v2 audience/issuer only when the registration actually issues v2 tokens.
No Entra application role check is performed. Azure RBAC grants to the Function's managed identity
Expand All @@ -66,10 +74,10 @@ The script alone does not make this implementation conform to every Preview 1 re
- The guide requires voice digits to be spoken separately. This implementation preserves the supplied
message; verify the selected voice API's behavior rather than assuming unspaced digits are intelligible.

The pasted script also needs its advertised 100-byte UTF-8 endpoint-URL check before deployment.
A public-only certificate cannot supply the private key it later exports. Treat failed infrastructure
role assignments as failures unless the exact assignment is verified as already present. Verify these
script prerequisites separately; the application tests do not validate provisioning.
The guided deployment includes explicitly labelled dummy provider values for configuration testing.
These values are written into the actual Function App environment; they do not establish provider
connectivity. Replace the selected route with provider-approved settings, provision Telesign Key
Vault credentials or Soprano provider consent as applicable, and verify deployed security controls.

## 2. Provision encryption and deployment trust

Expand Down
Loading