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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Guided setup deploys **one regional endpoint** using JavaScript by default, with
> Start in a separate nonproduction environment. Confirm provider access and Microsoft's approved
> test and activation process before deployment.

## Table of Contents
## Table of contents

- [Features](#features)
- [Requirements](#requirements)
Expand All @@ -30,6 +30,8 @@ Guided setup deploys **one regional endpoint** using JavaScript by default, with
- Use a separate test environment with a Microsoft Entra ID tenant, Azure subscription, and
unique resource name prefix. Supply a dedicated app registration or create one during guided setup.
- Check [roles, permissions, tools, regional availability, and quota](setup/docs/README.md#prerequisites-for-step-2).
- For Telesign, complete the [OAuth profile placeholders](docs/ONBOARDING.md#telesign-oauth-configuration-and-handoff)
before deployment.
- Plan for Azure, telemetry, and provider charges. The [cost estimate](docs/COST-CALCULATOR.md) is not a bill.
- Use Easy Auth to allow only the approved Microsoft caller.

Expand Down
25 changes: 15 additions & 10 deletions TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ It covers capacity, shared keys, dependencies, readiness, regional recovery dril
## Implementations

| Language | Status | Folder |
|----------|--------|--------|
| --- | --- | --- |
| JavaScript (Node.js) | Available | [javascript/](javascript/) |
| C# (.NET isolated worker) | Available | [dotnet/](dotnet/) |
| Python (v2 model) | Available | [python/](python/) |
Expand Down Expand Up @@ -70,7 +70,7 @@ Guided setup already publishes the package. Use this section for manual deployme
Download the latest successful CI ZIP for your chosen language:

| Language | Download | Contents |
|---|---|---|
| --- | --- | --- |
| JavaScript | [epp-javascript.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest/download/epp-javascript.zip) | Application and production dependencies |
| .NET | [epp-dotnet-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest/download/epp-dotnet-source.zip) | C# Function source and project file. Build and publish before deployment. |
| Python | [epp-python-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest/download/epp-python-source.zip) | Source for Azure remote build on Linux |
Expand All @@ -81,12 +81,12 @@ Verify `SHA256SUMS.txt`, configure the runtime, settings, Key Vault, and **Easy
Successful `main` builds publish a versioned [release](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest).
PR builds publish workflow artifacts only. Packaging does not deploy or verify delivery.

## Build ZIPs Locally
## Build ZIPs locally

Run one packaging script from the repository root. It creates a local ZIP without changing Azure.

| Language | Root-level script | Prerequisites | ZIP in `artifacts/` |
|---|---|---|---|
| --- | --- | --- | --- |
| JavaScript | [package-javascript.ps1](package-javascript.ps1) | PowerShell 7+, Node.js 20 or 22 with npm, npm registry access | `epp-javascript.zip` |
| .NET | [package-dotnet.ps1](package-dotnet.ps1) | PowerShell 7+ to package. .NET 8 SDK and NuGet feed access when customers build. | `epp-dotnet-source.zip` |
| Python | [package-python.ps1](package-python.ps1) | PowerShell 7+. Azure remote build required when deploying. | `epp-python-source.zip` |
Expand Down Expand Up @@ -228,7 +228,7 @@ For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript,
`config.provider_name` in Python, and `config.ProviderName` in .NET.

| Variable | When needed | Value |
|---|---|---|
| --- | --- | --- |
| `AzureWebJobsStorage` | Functions host storage | Local sample: `UseDevelopmentStorage=true` with Azurite running. Configure Azure host storage separately for the selected plan. |
| `FUNCTIONS_WORKER_RUNTIME` | Local host / EP1 | `node`, `python`, or `dotnet-isolated`. FC1 uses `functionAppConfig.runtime` instead. Do not add this EP1 setting to FC1. |
| `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. Guided setup pins the PEM backing secret of its Key Vault certificate. |
Expand All @@ -245,6 +245,13 @@ For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript,
- **OAuth:** `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE`, `EPP_OUTBOUND_CLIENT_ID`, and
`EPP_OUTBOUND_MI_CLIENT_ID`.

For OAuth, setup maps profile `deployment.AccessTokenScopeTenant` to `EPP_PROVIDER_TENANT_ID`.
The adjacent `deployment.tenantId` remains the provider's Security Store tenant and identifies
the provider during application discovery. If an older profile omits `AccessTokenScopeTenant`,
setup uses `tenantId`; explicit empty, invalid, or placeholder values fail validation.
Both provider tenants are included in the reviewed app allowlist, preserving existing entries.
This does not change inbound Easy Auth or use request `tenantId` as a source of trust.

All runtimes use `ManagedIdentityCredential` for Key Vault and outbound token access. They do not
fall back to a developer's Azure CLI identity.

Expand Down Expand Up @@ -363,11 +370,9 @@ locale or `en-US`, and sends `beforePasswordText`, `password`, and `afterPasswor

### Telesign

Telesign uses `telesign-customer-id` and `telesign-api-key` from Key Vault. Basic authentication is:

```text
base64(UTF8(customer-id:api-key))
```
Telesign uses OAuth only, with the same managed-identity client-assertion exchange and settings as
[Soprano](#soprano). Complete the
[Telesign profile](docs/ONBOARDING.md#telesign-oauth-configuration-and-handoff) before deployment.

The adapter sends `recipient.phone_number`, `message.text`, optional `message.language`, one
`channels[].channel`, and `correlation_id`. SMS forwards the message unchanged. Voice separates
Expand Down
6 changes: 3 additions & 3 deletions docs/FRONTDOOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ authentication settings consistent and use the same RSA key. Match key material,
or version IDs.

| Component | Purpose |
|---|---|
| --- | --- |
| Front Door profile and endpoint | Provide the single hostname used by the caller. One endpoint is sufficient for regional redundancy. |
| Route | Match `/api/SendOtp` and forward the request to the EPP origin group over HTTPS. |
| Origin group | Contain the regional Function hostnames and their health, priority, weight, and latency settings. |
Expand Down Expand Up @@ -182,7 +182,7 @@ In the Azure portal, create a Front Door Standard/Premium profile, an endpoint,
Use these starting settings, then validate them against the required performance target:

| Setting | Starting value |
|---|---|
| --- | --- |
| Tier and domain | Standard, using its generated `azurefd.net` hostname |
| Origin hostnames | Each Function App's actual default hostname |
| Origin host header | That origin's Function hostname, not the Front Door hostname |
Expand Down Expand Up @@ -257,7 +257,7 @@ sends only inside this endpoint. Caller retries or other delivery paths can stil
caller behavior with Microsoft and obtain separate approval for sign-in or delivery tests.

| What to test | How and where to test safely | Expected outcome / acceptance | Evidence to keep |
|---|---|---|---|
| --- | --- | --- | --- |
| Baseline encrypted evaluation | Send synthetic encrypted `mode: 2` requests through Front Door with both origins healthy. Confirm each origin serves requests. | Matching nonce checked locally, no delivery, and agreed baseline error/latency limits met. | Offered/completed rates, latency/errors, safe correlation IDs, and serving region. Exclude nonce values and request bodies. |
| Bad caller credentials, unauthorized callers, and direct-origin access | From the test harness, try missing/invalid/wrong-audience tokens, an unauthorized app, and direct-origin requests including a spoofed `X-Azure-FDID`. Send valid evaluations before and after. | Invalid calls are rejected while valid evaluations still work. Authentication and ingress restrictions remain enabled. | Response status and sanitized access diagnostics identifying the rejection, plus successful surrounding evaluations. |
| Recovery from A to B | Follow the recovery test: isolate only approved test origin A while leaving it **enabled in Front Door**. Keep B healthy and evaluation traffic bounded. | Health-based routing to B meets agreed recovery and transition-error limits, without changing the public URL, trust, or encryption key. | Fault/probe/client timestamps, every failure/timeout, and B's request/health signals. |
Expand Down
6 changes: 3 additions & 3 deletions docs/LOGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ preservation. Field names differ by runtime, so verify the
A successful live request emits these events in order:

| Service event | Safe information recorded |
|---|---|
| --- | --- |
| `request_received` | Function invocation and available raw Microsoft trace IDs under their `x-ms-*` names. Excludes the raw body and arbitrary headers. |
| `envelope_validated` / `payload_validated` | Allowlisted body metadata: validated payload type, normalized `channel`, `evaluation` and optional `ttlSeconds`. |
| `delivery_context_decrypted` | Decryption completed. Excludes plaintext fields, JWE, and key ID. |
Expand Down Expand Up @@ -90,15 +90,15 @@ request ID joins events even when
Microsoft IDs are absent or change. Log names deliberately distinguish these sources:

| Log field | Source and meaning |
|---|---|
| --- | --- |
| `functionName` | Runtime function name: `SendOtp` in JavaScript/.NET, `send_otp` in Python. The HTTP route remains `/api/SendOtp` in every runtime. |
| `functionRequestId` | Generated by this handler. Matches `requestId` in failure responses. It is not Microsoft's request ID. |
| `functionInvocationId` | Azure Functions host invocation ID. Null for direct handler calls without a host context. |
| `x-ms-client-request-id` | Raw Microsoft per-attempt ID from the header of the same name, not the generated Function request ID used by dispatch. |
| `x-ms-correlation-id` | Raw selected Microsoft correlation: envelope `correlationId` first, then the header of the same name. The existing precedence is unchanged, and this never contains a generated Function ID. |
| `msCorrelationIdSource` | `envelope`, `header`, or `none`. Identifies the selected value when the envelope and header differ. |
| `providerMessageId` | Raw provider-normalized message/reference ID returned by the provider, including Telesign `reference_id`, for support lookup. Not filled from delivery or Function IDs, although a provider may echo an ID it received. |
| `providerTenantId` | Raw configured `EPP_PROVIDER_TENANT_ID` for OAuth, not incoming envelope `tenantId`. |
| `providerTenantId` | Configured OAuth token tenant (`EPP_PROVIDER_TENANT_ID`), not incoming envelope `tenantId`. Unlike this log field, the deployment summary's `providerTenantId` identifies the Security Store tenant. |
| `functionOutboundClientId` | Raw application's `EPP_OUTBOUND_CLIENT_ID` used for provider access, not a Microsoft request ID. Guided setup reuses the endpoint app client ID here. Its outbound purpose is distinct from the inbound audience setting. |
| `functionOutboundManagedIdentityClientId` | Raw configured `EPP_OUTBOUND_MI_CLIENT_ID` used to obtain the app assertion, not the Key Vault identity or the identity's principal/Object ID. |
| `providerEndpoint` | Validated final outbound URL without userinfo, query string, or fragment. Scheme, host/port, and API path remain visible. |
Expand Down
44 changes: 29 additions & 15 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ the approved plan. `policyChanged: false` is expected because setup does not act
| `EPP_PROVIDER_NAME` | Selects the registered adapter | Matches the approved provider |
| `EPP_PROVIDER_SMS_ENDPOINT`, `EPP_PROVIDER_VOICE_ENDPOINT` | Channel-specific provider URLs | Provider confirms access to each channel |
| `EPP_PROVIDER_ENDPOINT_REGION` | Provider route label | Matches Global or EU selection |
| `EPP_PROVIDER_AUTH_MODE` | `apiKey` for Telesign or `oauth` for Infobip/Soprano | Matches the selected profile |
| `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, `EPP_PROVIDER_SCOPE` | OAuth provider API tenant, app, and scope | Provider administrator confirms exact values |
| `EPP_PROVIDER_AUTH_MODE` | `oauth` for Infobip, Soprano, or Telesign | Matches the selected profile |
| `EPP_PROVIDER_TENANT_ID` | OAuth token-issuing tenant from `deployment.AccessTokenScopeTenant` | Can differ from the Security Store `deployment.tenantId`; summary records the two as `accessTokenScopeTenant` and `providerTenantId` |
| `EPP_PROVIDER_APP_ID`, `EPP_PROVIDER_SCOPE` | OAuth provider API application ID and resource scope ending in `/.default` | Provider administrator confirms exact values |
| `EPP_OUTBOUND_CLIENT_ID`, `EPP_OUTBOUND_MI_CLIENT_ID` | Customer app and outbound identity used for OAuth federation | Do not substitute object IDs |
| `KEY_VAULT_URL` | Vault holding API-key provider credentials | Use the summary's vault URI |
| `EPP_DECRYPTION_KEY_PEM`, `EPP_ENCRYPTION_KEY_ID` | Versioned encryption key reference and registered key ID | Assign a renewal owner |
Expand All @@ -110,7 +111,7 @@ Guided setup supports **Infobip, Soprano, and Telesign**.
| --- | --- | --- |
| Infobip | OAuth client assertion through managed identity federation | Complete provider-side authorization and configure an approved sender/caller ID in `EPP_PROVIDER_ACCOUNT_NAME`; no Infobip API-key secret |
| Soprano | OAuth client assertion through managed identity federation | Complete provider-side authorization and confirm tenant, app, scope, account, and channels |
| Telesign | Basic authentication from Key Vault secrets | Add `telesign-api-key` and `telesign-customer-id` to the setup-created vault |
| Telesign | OAuth client assertion through managed identity federation | Complete [OAuth configuration and handoff](#telesign-oauth-configuration-and-handoff) |

Sinch is bundled but does not have a guided setup profile. Twilio requires the
[Add Twilio](BRING-YOUR-OWN-PROVIDER.md) development process.
Expand All @@ -120,44 +121,57 @@ than API-key authentication. Its channel settings are HTTPS regional base URLs,
message paths in the Security Store manifest. See
[Infobip routing and migration](../TECHNICAL.md#infobip).

### Telesign: OAuth configuration and handoff

The provider owner must complete `deployment.AccessTokenScopeTenant` and both Global/EU routes'
`appId` and `scope` in the [Telesign profile](../setup/providers/telesign.json) before deployment.
Scopes must end in `/.default`. Keep `deployment.tenantId` as the Security Store tenant.

Use a Function package supporting Telesign OAuth and complete the
[provider-administrator handoff](#oauth-provider-administrator-handoff).

<a id="where-to-enter-api-key-credentials-in-key-vault"></a>
### Add Telesign secrets
### Add API-key provider secrets

This applies to API-key adapters such as Sinch, not the guided OAuth providers. Use the exact
secret names required by the selected adapter.

1. In the deployment summary, find `resources.keyVault`.
2. Open that vault in **Azure portal > Key vaults**.
3. Select **Objects > Secrets > Generate/Import**.

![Key Vault Objects menu with Secrets selected and the Generate/Import action visible](images/onboarding/key-vault-secrets.png)

4. Create `telesign-api-key` with the raw API key.
5. Create `telesign-customer-id` with the matching raw Customer ID.
6. Verify both secret names, enabled state, dates, and versions without viewing their values.
7. Confirm the Function's system-assigned identity has **Key Vault Secrets User**.
4. Create the required secret with the raw credential, such as `sinch-api-token` for Sinch.
5. Verify its name, enabled state, dates, and version without viewing its value.
6. Confirm the Function's system-assigned identity has **Key Vault Secrets User**.

![Unsaved Create a secret form showing a sample name, masked placeholder value, and Enabled set to Yes](images/onboarding/key-vault-create-secret.png)

*The screenshots contain test examples and fake values.*

Do not encode the credential pair or enter an Authorization header. The adapter builds Basic
authentication. Keep credentials out of commands, transcripts, settings, screenshots, and chat.
Store raw credentials rather than an Authorization header. Keep credentials out of commands,
transcripts, settings, screenshots, and chat.

<a id="telesign-enter-and-verify-the-two-vault-secrets"></a>
During rotation, coordinate both values and their provider validity window. Verify an approved live
test before retiring the old pair. Evaluation does not test provider credentials.
During rotation, coordinate credential updates and the provider validity window. Verify an approved live
test before retiring the old credentials. Evaluation does not test provider credentials.

<a id="soprano-provider-administrator-handoff"></a>
<a id="complete-soprano-authorization"></a>
### Complete Infobip or Soprano authorization
<a id="complete-infobip-or-soprano-authorization"></a>
<a id="oauth-provider-administrator-handoff"></a>
### Complete provider OAuth authorization

Setup creates the customer-side federation. The provider administrator still needs to authorize the
customer application and account.
customer application and account in the configured access-token tenant.

Send identifiers only through the approved onboarding route:

- customer tenant ID
- endpoint application client ID
- intended channels and provider URLs
- `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, and `EPP_PROVIDER_SCOPE`
- Security Store tenant ID when it differs from the access-token tenant
- outbound identity client ID and principal ID when troubleshooting federation
- provider-requested account or sender reference

Expand Down
Loading
Loading