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
55 changes: 15 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The same Function endpoint supports both SMS and voice. Each request's `channel`
configured provider URL, and the SMS and voice URLs can differ. Validate every channel you intend
to activate.

The screenshots show the Azure portal and the setup script. You can also register the application
The screenshots show the Azure portal. You can also register the application
in the Microsoft Entra admin center. Identifying details are hidden; your portal layout may differ.

## 1. Get ready
Expand Down Expand Up @@ -111,19 +111,10 @@ Setup first lists cached Azure CLI user subscriptions. Select the customer tenan
then select or enter the dedicated application client ID and choose an Azure region. Refresh or
sign in when the expected account is missing.

Choose the provider route and provider, then **one** platform: Node.js (JavaScript), .NET (C#), or Python.
When `-Provider` is omitted, setup reads the application's allowed-tenant configuration: no
supported provider means enter a provider name, one means confirm it, and several means choose
one by name. Provider choices are never numbered, and existing allowed provider tenants are preserved.
Setup configures both SMS and voice provider URLs without asking you to select a channel.
It downloads and verifies the package, builds it if needed, and deploys it for you.
You do not need a separate ZIP upload or local settings file.

![Earlier PowerShell setup prompts for tenant, subscription, application, region, and SMS or voice, with identifiers hidden](docs/images/onboarding/setup-identifiers.png)

*Use the dedicated application created in Step 2 and your intended Azure region. This screenshot
is from an earlier script version; current setup uses selection lists for the tenant, subscription,
application, and region, and no longer has the channel prompt. It is an input screen, not a deployment result.*
Choose the provider route and enter or confirm the provider **by name**, then choose **one**
platform: Node.js (JavaScript), .NET (C#), or Python. Setup configures both SMS and voice URLs;
there is no channel prompt. It downloads and verifies the package, builds it if needed, and
deploys it for you. You do not need a separate ZIP upload or local settings file.

Choose **FC1** for on-demand hosting that can scale to zero, or **EP1** for paid warm capacity.
FC1 can cold-start, and its free usage grant does not make the whole deployment free.
Expand All @@ -132,24 +123,13 @@ In **Tenant scope**, choose Global or EU for your provider route. This does not
Azure region or guarantee data residency.
For the resource prefix, use 2-8 lowercase letters or digits, starting with a letter.

![Cropped setup choices for provider route, provider, language, and hosting plan, with provider names hidden](docs/images/onboarding/setup-choices.png)

*Choose the options approved for your deployment; do not copy the example selection numbers.
The Global/EU choice is not the Azure region. The hosting-plan warning applies even when a
plan has a free usage grant.*
Complete any required CLI sign-in, MFA, or administrator consent yourself. Setup checks
prerequisites and prepares the deployment plan; resolve any reported failure before continuing.

Next, setup checks prerequisites, revalidates the Azure CLI and Microsoft Graph context, verifies
the selected package, and prepares the deployment plan. For C#, this includes an automatic local build.
Complete any required CLI sign-in, MFA, or administrator consent yourself. If a permission or prerequisite check fails, resolve it
before continuing.

Setup also checks the application's existing **encryption certificates** before deployment.
If one exists, choose to reuse it and supply its **versioned Key Vault certificate URL**, or
replace it. If several exist, select one to keep and approve removal of the others, or replace
them all with one new certificate. Reuse preserves the selected key ID rather than adding another
credential. See [certificate selection](setup/docs/README.md#encryption-certificate-selection)
for the URL, permissions, and unattended options. Coordinate maintenance before changing an
active application's encryption credentials; every endpoint sharing that application is affected.
For a new application, setup creates its encryption certificate. If encryption credentials
already exist, follow [certificate selection](setup/docs/README.md#encryption-certificate-selection)
to reuse one exact Key Vault certificate version or approve replacement. Coordinate maintenance
first: changing these credentials affects every endpoint sharing the application.

**Review the complete deployment plan before typing `Yes`.** Check the accounts, region,
SMS and voice provider URLs, platform, plan, cache settings, resource names, permissions, and
Expand All @@ -159,17 +139,12 @@ application-read permission. `No` or Enter cancels deployment; sign-in and prere
are separate from this approval.

**Want to review without deploying?** Run interactively and choose **No** at that final prompt.
There is no `-DryRun` or `-WhatIf` switch. This is not an offline dry run: sign-in, prerequisite
checks, downloads, and template compilation happen first. Do not pass `-ApproveDeployment`
when you only want to review the plan.
This still performs online preflight and downloads. There is no `-DryRun` or `-WhatIf` switch;
see [plan review](setup/docs/README.md#reviewing-the-plan-without-deploying) for details.

After you approve, leave the script running while it configures the application, creates the
Azure resources, and publishes the code. Wait for **Deployment completed** and the saved summary
before moving to Step 4. The input screenshots do not show a completed deployment.

Normal console output ends with **Next steps** after resource generation. Use
`.\Setup-Epp.ps1 -PassThru` when you also need the structured deployment result in a variable
or pipeline; the summary JSON is always saved.
before moving to Step 4.

Setup creates the Function App, storage, Key Vault, identities, and monitoring resources.
**Do not create these resources manually first.**
Expand Down Expand Up @@ -265,5 +240,5 @@ Deleting Azure resources does not undo an authentication policy change.
| Understand the architecture, settings, or packages | [Technical reference](TECHNICAL.md) and [HTTP contract](docs/CONTRACT.md) |
| Explore multiple Azure regions | [Manual Front Door guide](docs/FRONTDOOR.md); not included in guided setup |
| Build a custom implementation | [JavaScript](javascript/README.md), [C#](dotnet/README.md), or [Python](python/README.md) |
| Implement the SAS-facing BYOT interface | [BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md); caller authentication, encrypted payload, and responses, with customer-owned service integration |
| Implement the Entra ID-facing BYOT interface | [BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md); caller authentication, encrypted payload, and responses, with customer-owned service integration |
| Design primary-to-secondary provider fallback | [Developer walkthrough](docs/MULTI-PROVIDER-IMPLEMENTATION.md); not a built-in feature |
32 changes: 17 additions & 15 deletions TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ For local development, manual configuration, and deployment checks, see the
[detailed configuration and validation guide](docs/ONBOARDING.md).

For the provider-neutral inbound interface, read the
[BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md). It covers the SAS envelope,
[BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md). It covers the Entra ID envelope,
caller authentication, encrypted delivery context, and endpoint responses. The outbound service
integration remains customer-owned; the guide does not add an adapter or guided setup support.

Expand Down Expand Up @@ -163,16 +163,16 @@ 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) →
Microsoft Entra ID → Easy Auth → anonymous HTTP handler (`POST /api/SendOtp`, validate envelope + decrypt JWE) →
configured provider (API key or 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
non-delivery path: after platform authentication, validate and decrypt, then echo the nonce without
calling a provider.

See [docs/CONTRACT.md](docs/CONTRACT.md) for the full specification every implementation follows.

Request `tenantId`, `channel`, `mode` and `ttlSeconds` are request data, not extra environment settings.
The trusted tenant issuer, endpoint-app audience and authorized SAS caller are configured in Easy Auth,
The trusted tenant issuer, endpoint-app audience and authorized Microsoft caller are configured in Easy Auth,
not in application environment settings or incoming request data.

## Configure environment variables
Expand Down Expand Up @@ -253,10 +253,12 @@ credential acquisition. Platform-managed identity caching and decryption-key ref
Core Tools does not resolve Azure Key Vault reference expressions locally. Supply the local test PEM
or base64 PEM directly; use a reference such as `@Microsoft.KeyVault(SecretUri=https://<vault>.vault.azure.net/secrets/<private-key-secret>/)`
for `EPP_DECRYPTION_KEY_PEM` in Azure app settings, where the platform resolves it.
Guided setup instead uses a **versioned** reference to
`secrets/phone-provider-encryption/<version>`, containing a certificate and its exportable RSA private
key in PEM format. A new Key Vault certificate version does not automatically switch the Function
or update Entra. See [certificate lifecycle](setup/docs/README.md#encryption-certificate-lifecycle).
Guided setup uses the **versioned** backing-secret URL recorded as `certificateSecretId` in the
deployment summary. Initial creation uses `phone-provider-encryption`; replacement uses a suffixed
name, and reuse preserves the selected certificate's name and vault. The secret contains the
certificate and its exportable RSA private key in PEM format. Do not reconstruct its URL from the
certificate name or version. A new Key Vault certificate version does not automatically switch
the Function or update Entra. See [certificate lifecycle](setup/docs/README.md#encryption-certificate-lifecycle).

Configure inbound issuer/audience/caller trust in **Easy Auth**, not these application variables.
Incoming `tenantId`, `channel`, `mode` and `ttlSeconds` are request data and never override the
Expand All @@ -276,16 +278,16 @@ The `telesign` adapter sends its JSON contract to the complete SMS or voice URL
request channel from the configured endpoints. 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
`telesign-customer-id` and `telesign-api-key`. Digest and provider-token authentication are not
implemented. The incoming caller's Authorization header is never forwarded.

The adapter builds the following JSON from the decrypted delivery context and envelope:
For an SMS request, the adapter builds the following JSON from the decrypted delivery context and envelope:

```json
{
"recipient": { "phone_number": "+1234567890" },
"message": { "text": "Your verification code is 4821", "language": "en" },
"channels": [{ "channel": "voice" }],
"channels": [{ "channel": "sms" }],
"correlation_id": "unique-string-123"
}
```
Expand Down Expand Up @@ -313,8 +315,8 @@ include only the provider HTTP status, mapped status/outcome and the bounded raw
ID as `providerMessageId` for support lookup, never the raw response or description. Existing numeric success codes
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
provider acceptance, not handset receipt or completed audio playback. The sample's recognized
codes are not a complete provider status-code catalog. Missing, malformed, or
unknown codes fail closed, as do unsuccessful HTTP responses. Confirm the status-code catalog and
account access with Telesign before production.

Expand All @@ -323,12 +325,12 @@ account access with Telesign before production.
**Easy Auth (App Service Authentication) is the only caller-authentication gate, before the anonymous
Function.** Enable it with `requireAuthentication=true`, `unauthenticatedClientAction=Return401` and
`requireHttps=true`. Configure the trusted tenant issuer and `allowedAudiences` for the endpoint app,
and a **nonempty `allowedApplications`** list pinned to the authorized SAS caller application ID.
and a **nonempty `allowedApplications`** list pinned to the authorized Microsoft caller application ID.
Do not exclude the SendOtp path. The handler does not parse or validate bearer tokens, and there is
no backup application validation or function-key gate. **Never expose this endpoint to the public
internet with Easy Auth disabled or bypassed.** See [platform setup](docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust).

JWE decryption protects the payload but **does not authenticate SAS**: anyone with the public key can
JWE decryption protects the payload but **does not authenticate the caller**: anyone with the public key can
encrypt a request. A nonce echo, including a fixed nonce, is not caller authentication. Provider API
keys are read from **Key Vault** via **managed identity**; they authenticate the outbound provider call,
not the inbound request.
Expand Down
31 changes: 15 additions & 16 deletions docs/BRING-YOUR-OWN-PROVIDER.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# BYOT integration: the SAS request and response
# BYOT integration: the Entra ID request and response

**SAS (Strong Authentication Service)** sends your **External Phone Provider (EPP)** endpoint
**Microsoft Entra ID** sends your **External Phone Provider (EPP)** endpoint
an encrypted phone number and one-time passcode message. Your endpoint verifies the caller,
decrypts the details, and either evaluates the request without sending or uses your phone service.
It then responds to SAS. **Your team owns the connection to that service.**
It then responds to Entra ID. **Your team owns the connection to that service.**

Before deploying, ask your tenant administrator to confirm
[eligibility, testing, and activation with Microsoft](ONBOARDING.md#before-purchasing-or-deploying).
Expand All @@ -13,13 +13,13 @@ Deploying the endpoint alone does not enable tenant policy.

## 1. Verify the caller

SAS sends **`POST /api/SendOtp` over HTTPS**, with a signed Entra bearer token in `Authorization`.
Entra ID sends **`POST /api/SendOtp` over HTTPS**, with a signed bearer token in `Authorization`.
In this sample, **Easy Auth** (Azure App Service Authentication) checks the token and authorizes
the caller before the Function runs. The Function's trigger is anonymous and has no backup
authentication check, so do not expose it with Easy Auth disabled or bypassed.

Require HTTPS and authentication, return `401` for unauthenticated requests, and restrict access
to the trusted tenant issuer, endpoint-app audience, and authorized SAS caller application ID.
to the trusted tenant issuer, endpoint-app audience, and authorized Microsoft caller application ID.
Your Azure operator should use the endpoint app registration and
[approved setup configuration](../setup/support/Epp.Setup.psm1) for these values, then verify
**Function App > Authentication**. Follow the
Expand All @@ -28,9 +28,7 @@ caller-ID guidance, and token-version details.

**Encryption is not caller authentication.** Anyone with the public key can create a JWE
(JSON Web Encryption) payload. Neither its nonce, the body's `tenantId`, nor tracing or
forwarded principal headers prove that SAS sent it.

<a id="2-read-the-sas-envelope"></a>
forwarded principal headers prove that Entra ID sent it.

## 2. Read and decrypt the request

Expand Down Expand Up @@ -93,7 +91,7 @@ Configured workers may independently prepare or refresh credentials, but that do
**Live (`mode: 1` or `"live"`):** your team implements and maintains the service integration.
Map the requested SMS or voice delivery to your service, preserving the destination, message,
and passcode. Handle voice rendering and locales as needed. Authenticate with your own service
credentials; **never forward SAS's bearer token**.
credentials; **never forward the incoming bearer token**.

Wait for recognized acceptance before returning success. A queued or pending result counts
only if your integration recognizes it as acceptance. Unknown or incomplete responses must fail,
Expand All @@ -104,9 +102,7 @@ The sample uses one configured service per deployment. A custom integration does
guided setup support. Follow [Where to add your integration](#where-to-add-your-integration)
for the files to create, registration, settings, and packaging.

<a id="5-respond-to-sas"></a>

## 4. Respond to SAS
## 4. Respond to Entra ID

Return **HTTP `200` with a JSON object containing exactly these fields** after a successful
evaluation or recognized live acceptance:
Expand All @@ -122,8 +118,11 @@ Correlation header sanitization and empty-value handling differ by runtime; see
the internal attempt `messageId`, with a generated fallback. These IDs do not provide deduplication.

**For live requests, echo the nonce only after acceptance, not before sending.**
Acceptance is not proof of handset receipt. SAS treats a `2xx` response with a matching nonce
as handled; a mismatched nonce, non-`2xx` response, or timeout causes SAS to fall back to native delivery.
Acceptance is not proof of handset receipt. The sample's integration contract expects Entra ID to
recognize a `2xx` response with a matching nonce; failures or timeouts can lead to native fallback.
This repository does not implement or verify the Entra ID caller's current fallback or retry policy.
Confirm that behavior through the [approved test procedure](ONBOARDING.md#validate-the-deployed-endpoint),
including whether a lost response can lead to another send.

On failure, omit both the nonce and accepted status. Handler errors contain `error` and a
generated `requestId`, plus a fixed `reason` for validation errors and `correlationId` where
Expand All @@ -136,8 +135,8 @@ errors can occur before the handler and need not use this JSON shape. Use the

Choose **one runtime** and work in its source directory. Keep Easy Auth and the shared handler's
envelope validation, JWE decryption, evaluation branch, and nonce responses. Your provider code
builds the outbound request and interprets the service result; it does not authenticate SAS or
write the HTTP response to SAS.
builds the outbound request and interprets the service result; it does not authenticate the
incoming caller or write the HTTP response to Entra ID.

### Add and register the provider

Expand Down
Loading
Loading