Skip to content

Commit 738f9c1

Browse files
author
Hou Chi Chan
committed
Merge main into Soprano JWT branch and resolve conflicts
2 parents f600e1e + 7b820ca commit 738f9c1

13 files changed

Lines changed: 524 additions & 334 deletions

File tree

‎README.md‎

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# External Phone Provider — Azure Function Sample
1+
# External Phone Provider: Azure Function Sample
22

33
A provider-agnostic **OTP-delivery Azure Function** sample, implemented across multiple languages.
44
Each language folder is a self-contained implementation of the **same design and the same
5-
[contract](docs/CONTRACT.md)** — one engine, drop-in provider adapters, env-provisioned config, and
5+
[contract](docs/CONTRACT.md)**: one engine, drop-in provider adapters, env-provisioned config, and
66
secrets in Key Vault.
77

88
## Implementations
@@ -14,15 +14,15 @@ secrets in Key Vault.
1414
| Python (v2 model) | Available | [python/](python/) |
1515

1616
All implementations conform to the **language-agnostic contract** in
17-
[docs/CONTRACT.md](docs/CONTRACT.md) — identical HTTP API, provider-adapter shape, config/env var
17+
[docs/CONTRACT.md](docs/CONTRACT.md): identical HTTP API, provider-adapter shape, config/env var
1818
names, Key Vault secret names, and behaviors (fail-closed, managed identity, privacy). Pick any folder
1919
and follow its README.
2020

2121
Choose one language and configure the adapter for your provider. No provider is preferred or selected
2222
by default. Deploy each language separately, not all three to the same Function App. See the
2323
[shared configuration](docs/CONTRACT.md#default-provider-and-configuration-readers).
2424

25-
New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — setup, config, running, securing,
25+
New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** for setup, config, running, securing,
2626
and deploying, step by step.
2727

2828
## Download a Function ZIP
@@ -142,7 +142,7 @@ how code accesses configuration, not the environment-variable names.
142142
| Variable | When needed | Value |
143143
|---|---|---|
144144
| `AzureWebJobsStorage` | Functions host storage | Local sample: `UseDevelopmentStorage=true` with Azurite running. Configure Azure host storage separately for the selected plan. |
145-
| `FUNCTIONS_WORKER_RUNTIME` | Functions host | `node`, `python`, or `dotnet-isolated`—exactly one value matching the chosen implementation. |
145+
| `FUNCTIONS_WORKER_RUNTIME` | Functions host | `node`, `python`, or `dotnet-isolated`. Choose the value matching your implementation. |
146146
| `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. |
147147
| `EPP_ENCRYPTION_KEY_ID` | Optional | Expected encryption key ID; mismatch only produces an advisory warning. |
148148
| `EPP_PROVIDER_NAME` | Live delivery | Selected adapter's manifest ID. No default provider. |
@@ -166,6 +166,8 @@ how code accesses configuration, not the environment-variable names.
166166
3. Store provider API keys and any required identity secrets in Key Vault using the **exact names in
167167
the adapter manifest**. Grant that app/slot's managed identity *Key Vault Secrets User* on those
168168
secrets. An API key in a local environment variable is not a supported replacement for the resolver.
169+
See the [provider credential naming table](docs/ONBOARDING.md#provider-credential-names) and
170+
[local use of existing cloud secrets](docs/ONBOARDING.md#local-settings-and-cloud-secrets).
169171

170172
Evaluation requests do not need provider variables or provider secrets. They still need the decryption
171173
key. The default credential resolvers use `ManagedIdentityCredential`, **not** the developer's CLI
@@ -267,6 +269,52 @@ in historical reports and are not evidence for this federated flow. Subsequent s
267269
to SDK log privacy have offline regression coverage, but are not included
268270
in this live result until redeployed and verified.
269271

272+
## Telesign EPP
273+
274+
The `telesign` adapter uses `POST https://verify.telesign.com/integration/msft/cyot`
275+
for both SMS and Voice. Set `EPP_PROVIDER_NAME=telesign` and
276+
`EPP_PROVIDER_ENDPOINT=https://verify.telesign.com` (the base URL, without the route).
277+
This replaces the legacy `/v1/messaging` and `/v1/voice` integrations in all three languages.
278+
279+
Basic authentication uses `base64(customer-id:api-key)`, with the existing Key Vault secrets
280+
`telesign-customer-id` and `telesign-api-key`. Digest and Phase 2 token authentication are not
281+
implemented. The incoming caller's Authorization header is never forwarded.
282+
283+
The adapter builds the following JSON from the decrypted delivery context and envelope:
284+
285+
```json
286+
{
287+
"recipient": { "phone_number": "+1234567890" },
288+
"message": { "text": "Your verification code is 4821", "language": "en" },
289+
"channels": [{ "channel": "voice" }],
290+
"correlation_id": "unique-string-123"
291+
}
292+
```
293+
294+
`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`; the leading `+` is preserved. The complete
295+
`message` is passed unchanged as `message.text`, including whitespace and OTP digit spacing.
296+
Telesign performs text-to-speech for Voice; no separate speech object or OTP extraction is needed.
297+
A nonblank string `locale` becomes `message.language`; otherwise language is omitted. The envelope
298+
channel selects the single `sms` or `voice` entry. `correlation_id` uses a nonempty string request
299+
correlation ID, falling back to the message ID for absent, empty, or non-string values. Reserved
300+
`account_lifecycle_event` and `originating_ip` fields are
301+
not sent; no client-IP inference or account-event default is applied. `TELESIGN_VOICE` and the
302+
legacy sender/form fields no longer affect this adapter.
303+
304+
Telesign's API supports `X-Shutter-Mode: true` for direct provider tests. The Function deliberately
305+
omits that header on live sends and does not forward it from incoming requests. Use the existing
306+
`mode: 2` evaluation path for Function tests without delivery: it skips provider HTTP and credential
307+
lookup entirely, rather than invoking Telesign shutter mode.
308+
309+
Responses normalize `reference_id` and `status.code`/`status.description` internally; provider
310+
metadata is not logged or exposed in the public nonce response. Existing numeric success codes
311+
are retained (SMS: 200, 203, 290-292; Voice: 100-103). CYOT code `3001` ("Message in progress"),
312+
observed for both channels, is also accepted on successful HTTP responses. This acknowledges
313+
provider acceptance, not handset receipt or completed audio playback. The supplied EPP integration
314+
overview does not provide a complete replacement status-code catalog. Missing, malformed, or
315+
unknown codes fail closed, as do unsuccessful HTTP responses. Confirm the status-code catalog and
316+
account access with Telesign before production.
317+
270318
## Security
271319

272320
**Easy Auth (App Service Authentication) is the only caller-authentication gate, before the anonymous
@@ -288,13 +336,13 @@ authentication; [separate deployed security checks](docs/ONBOARDING.md#4-package
288336

289337
## Docs
290338

291-
- **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — customer setup / run / secure / deploy guide.
292-
- **[docs/CONTRACT.md](docs/CONTRACT.md)** — the language-agnostic contract every implementation follows.
339+
- **[docs/ONBOARDING.md](docs/ONBOARDING.md)**: customer setup, security, deployment, and validation.
340+
- **[docs/CONTRACT.md](docs/CONTRACT.md)**: the language-agnostic contract every implementation follows.
293341

294342
## Contributing a language or provider
295343

296344
- **New provider** (in any language): add one adapter file exposing `manifest` + `buildRequest` +
297-
`parseResponse` — no engine changes. See the language folder's README.
345+
`parseResponse`; no engine changes. See the language folder's README.
298346
- **New language**: mirror the folder structure, implement the contract, add the same test scenarios,
299347
and wire it into [.github/workflows/ci.yml](.github/workflows/ci.yml).
300348

‎docs/CONTRACT.md‎

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# External Phone Provider Function — Language-Agnostic Contract
1+
# External Phone Provider Function: Language-Agnostic Contract
22

33
This defines the shared contract for [JavaScript](../javascript/), [Python](../python/) and
44
[.NET](../dotnet/). See [production limitations](#production-limitations) before production use.
@@ -34,7 +34,7 @@ Forwarded headers, including `x-ms-client-principal`, do not establish trust by
3434
replace the required Easy Auth gate. The handler does not use them to authenticate callers or forward
3535
the incoming `Authorization` header to the provider. Configure Easy Auth as described in section 5.
3636

37-
### EPP request body — `Envelope` (cleartext envelope)
37+
### EPP request body: `Envelope` (cleartext envelope)
3838

3939
| Field | Required | Notes |
4040
|-------|----------|-------|
@@ -265,19 +265,19 @@ success-looking status. Explicit `Block`/`StepUp` outcomes remain non-success re
265265
| `Fail` | `401` | provider returned 401/403 (auth) |
266266
| `Fail` | `400` | other provider 4xx |
267267
| `Fail` | `502` | other provider error, or missing credential/endpoint |
268-
| — | `504` | request to the provider timed out |
269-
| — | `502` | network error to the provider (non-timeout) |
268+
| N/A | `504` | request to the provider timed out |
269+
| N/A | `502` | network error to the provider (non-timeout) |
270270

271271
---
272272

273273
## 3. Provider adapter contract
274274

275275
Each provider is one unit exposing three things:
276276

277-
- **`manifest`** — protocol facts only:
278-
- `id` — provider id selected by `EPP_PROVIDER_NAME`; its base URL is `EPP_PROVIDER_ENDPOINT`
279-
- `auth` — `{ mode: 'apiKey', keyVaultSecretName, identityKeyVaultSecretName? }`; other modes fail closed
280-
- `responseMapping` — map of provider status → `Continue` | `Fail` | `Block` | `StepUp` (+ `default`)
277+
- **`manifest`**: protocol facts only:
278+
- `id`: provider id selected by `EPP_PROVIDER_NAME`; its base URL is `EPP_PROVIDER_ENDPOINT`
279+
- `auth`: `{ mode: 'apiKey', keyVaultSecretName, identityKeyVaultSecretName? }`; other modes fail closed
280+
- `responseMapping`: map of provider status → `Continue` | `Fail` | `Block` | `StepUp` (+ `default`)
281281
- **`buildRequest({ channel, endpoint, dispatch, credential, env })`** → `{ url, method, headers, body }`
282282
- **`parseResponse({ httpStatus, ok, json })`** → `ParsedResponse`, containing `success`,
283283
`providerHttpStatus`, optional `providerMessageId`, `providerStatusName`, `providerStatusCode`
@@ -298,8 +298,36 @@ class hierarchy is required.
298298

299299
Adapters require registration in the chosen runtime. Consult the selected adapter and its manifest
300300
for required credentials and options: the manifest declares secret names and protocol mappings;
301-
the implementation reads adapter-specific options from app settings. Do not duplicate individual
302-
API contracts or credential catalogs in shared onboarding documentation.
301+
the implementation reads adapter-specific options from app settings. Individual API contracts remain
302+
in the adapters; the [onboarding credential naming table](ONBOARDING.md#provider-credential-names)
303+
lists the exact manifest secret names for provisioning and authorized local tests. Keep that table
304+
aligned with the manifests; never include secret values in documentation or the settings sample.
305+
306+
### Telesign CYOT integration
307+
308+
SMS and Voice both use `POST https://verify.telesign.com/integration/msft/cyot` with JSON. Configure
309+
the base URL as `https://verify.telesign.com`. The adapter supplies `recipient.phone_number`, the
310+
unchanged `message.text`, optional `message.language`, one selected `channels[].channel`, and
311+
`correlation_id`. Keep the leading `+` in the E.164 phone number; the guide's example `12345678`
312+
does not satisfy its own required phone-number pattern.
313+
314+
Phase 1 supports Basic and Digest; this sample implements Basic only. Per
315+
[Telesign's authentication instructions](https://developer.telesign.com/enterprise/docs/authentication#basic-authentication),
316+
the header is `Authorization: Basic <base64(UTF8(customer-id:api-key))>`, using the raw Customer ID
317+
and API Key strings from Key Vault. Do not decode the API key first, send the API key alone, or
318+
substitute a key identifier. The guide's `Basic YOUR_API_KEY` is abbreviated, not the literal encoding.
319+
Provider-token authentication is described as Phase 2 and is not implemented for Telesign here.
320+
321+
The optional `account_lifecycle_event` and `originating_ip` fields are reserved for future intelligence
322+
capabilities. They are omitted; do not infer an originating address from the Function or synthesize
323+
account events to fill them.
324+
325+
Telesign's `X-Shutter-Mode: true` suppresses delivery at the provider while still calling its endpoint.
326+
It is appropriate for an explicitly authorized, direct provider diagnostic, not normal OTP delivery.
327+
The production adapter does not add or forward this header. Function evaluation mode remains separate:
328+
it validates/decrypts and skips all provider HTTP. A successful provider shutter probe is not evidence
329+
that an SMS was delivered or a Voice call was placed. Enabling the API globally also does not prove
330+
that a particular Customer ID/API Key pair is authorized for this integration.
303331

304332
---
305333

@@ -359,10 +387,10 @@ subscription activation and changing tenant policy belong to provisioning, not t
359387

360388
## 5. Required behaviors
361389

362-
- **Fail-closed** — only `Continue` → `200 accepted`; unknown status → `Fail`.
363-
- **Managed identity** — Key Vault access via managed identity only (user-assigned if `AZURE_CLIENT_ID`
390+
- **Fail-closed**: only `Continue` → `200 accepted`; unknown status → `Fail`.
391+
- **Managed identity**: Key Vault access via managed identity only (user-assigned if `AZURE_CLIENT_ID`
364392
set, else system-assigned). No static credentials.
365-
- **Privacy** — never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads,
393+
- **Privacy**: never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads,
366394
raw exceptions or provider responses. There is no plaintext diagnostic override. Each handler
367395
writes one summary with a generated request ID, the first 16 lowercase hex characters of the
368396
correlation ID's SHA256 hash, HTTP status,
@@ -373,7 +401,7 @@ subscription activation and changing tenant policy belong to provisioning, not t
373401
This distinguishes token attachment from API-key fallback; it does not expose token values or claims.
374402
A configured encryption-key-ID mismatch adds a fixed warning, never either key ID or the JWE header.
375403
Disable SDK, platform and proxy body tracing separately.
376-
- **Platform authentication only** — enable Easy Auth with `requireAuthentication=true`,
404+
- **Platform authentication only**: enable Easy Auth with `requireAuthentication=true`,
377405
`unauthenticatedClientAction=Return401` and `requireHttps=true`. Configure the trusted tenant issuer
378406
and `allowedAudiences` for the endpoint app, plus a **nonempty `allowedApplications`** list pinned to
379407
the authorized SAS caller application ID. No excluded path may bypass authentication for SendOtp.
@@ -383,7 +411,7 @@ subscription activation and changing tenant policy belong to provisioning, not t
383411
or bypassed.** Core Tools supplies no Easy Auth: local execution must bind only to loopback, with
384412
no tunnels or public forwarding. Neither request data, JWE decryption, a fixed nonce nor forwarded
385413
principal headers authenticate the SAS caller.
386-
- **Timeout boundaries** — platform authentication and Key Vault retrieval happen outside the outbound HTTP
414+
- **Timeout boundaries**: platform authentication and Key Vault retrieval happen outside the outbound HTTP
387415
timer. Python uses connect/read inactivity timeouts, not a hard elapsed-time deadline. The cap
388416
therefore does not guarantee a 3.2-second end-to-end response, especially on cold starts.
389417
A timed-out POST may already have been accepted; avoid blind retries that duplicate messages.

0 commit comments

Comments
 (0)