Implements the shared contract with a direct Azure Function flow and one selected provider per deployment. Target: Python 3.11, Azure Functions v4, Python v2 programming model.
For multiple regional origins behind one URL, see manual Front Door onboarding. No Front Door deployment script or readiness handler is supplied. The reported multi-region trials used JavaScript; validate an equivalent Python readiness implementation and deployment separately.
- Follow customer onboarding. Set
EPP_PROVIDER_NAMEto the selected provider id (<adapter-id>is only a placeholder). - Consult the selected provider in src/providers/ for required credentials and options. Store credentials in Key Vault under the provider-owned secret names, grant the Function's managed identity Key Vault Secrets User, and configure the matching endpoint/options.
- Base private local settings on ../docs/local.settings.sample.json,
replacing placeholders and selecting
FUNCTIONS_WORKER_RUNTIME=python. Put settings at the app root beside host.json. Configure decryption from the shared catalog and caller trust through Easy Auth, the only authentication gate before the anonymous Function. EnablerequireAuthentication=true,unauthenticatedClientAction=Return401andrequireHttps=true; pin the trusted tenant issuer, endpoint-appallowedAudiencesand a nonemptyallowedApplicationslist for the authorized SAS caller. Do not exclude SendOtp. There is no backup application token validation; never expose the endpoint to the public internet with Easy Auth disabled or bypassed. - Use a virtual environment, install requirements.txt and pytest, then run the offline tests/ from this folder. Start the local Functions host from this app root. Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding.
- Publish this folder to a compatible Linux Python Function App with dependencies or a supported remote build. Inspect the package and apply .funcignore; keep local settings and keys private. Offline tests cover application behavior, not platform authentication; run the separate deployed security checks.
Run Core Tools from python/. Create an untracked local.settings.json beside
host.json, starting from the shared sample.
For local evaluation, start Azurite and replace the test-key placeholder in this minimal setup:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "python",
"EPP_DECRYPTION_KEY_PEM": "<base64 of your local test private PEM>"
}
}For live delivery, add EPP_PROVIDER_NAME, the complete selected EPP_PROVIDER_ENDPOINT, and the
matching provider authentication settings to Values.
Add EPP_PROVIDER_ACCOUNT_NAME and any adapter-specific options only when required. Keep values as
strings, including optional EPP_PROVIDER_TIMEOUT_MS: "1500". Replace placeholders; provider API
keys belong in the provider-named Key Vault secrets, not this file. See the
complete variable table.
Core Tools loads Values into os.environ. Direct Python execution and pytest do not automatically
read local settings. read_config returns an AppConfig object; the handler/engine
use attributes such as config.provider_name, not dictionary key lookups. Restart the host after
settings change. Configure local host storage other than Azurite separately; do not copy the emulator
connection into Azure. Core Tools does not resolve Key Vault references locally; supply the local test
PEM or base64 PEM directly.
For Azure, set the same application variables on the serving app/slot's Environment variables → App settings page. Use a Key Vault reference for the private PEM. Provider secrets require managed identity, which is not supplied by a developer's CLI login. Use local evaluation or the mocked offline tests on an ordinary workstation, and bind local hosts only to loopback.
POST /api/SendOtp uses the same request and trust boundaries as the other runtimes. Incoming
mode, channel, ttlSeconds and tenantId are request data, not deployment authentication settings.
Easy Auth authenticates and authorizes the caller before the anonymous handler validates the envelope
and decrypts the JWE. Incoming Authorization is not parsed or echoed by the handler. JWE does not
authenticate SAS: anyone with the public key can encrypt a request, and a fixed nonce is not authentication.
Use incoming mode: 2 or mode: "evaluation" as the generic shutter for every provider: platform
authentication on Azure, handler validation and decryption run, but provider lookup, provider Key Vault
reads and provider HTTP do not. No provider configuration or diagnostic environment flag is required.
Live requests forward the rendered message unchanged using the configured provider's API key or OAuth token and
await acceptance before returning the nonce; failures omit it. Acceptance is not handset delivery.
Platform/key prerequisites and HTTP outcomes are defined in the
contract.
For Soprano voice, the adapter extracts the first six-digit passcode from the rendered message. It
uses a nonblank SAS request locale as the language, falling back to en-US, and sends fixed gender
1 and loop 2. These values require no additional environment settings. Soprano SMS continues to
forward the rendered message unchanged.
Telesign SMS also forwards the rendered message unchanged. Telesign voice comma-separates each six-digit numeric run that is not part of a longer number and repeats the complete paced message twice.
Worker initialization selects ApiKeyCache or AccessTokenCache from the provider's credential specification.
Only the selected cache starts: API keys use Key Vault and cachetools.TTLCache; access tokens use
the MI/Entra SDKs without Key Vault. One daemon loop polls every 30 seconds. Configuration changes
require restart. Callers can stop waiting without abandoning shared reads; synchronous SDK I/O uses connect/read
timeouts, not a total transport deadline. atexit stops refresh and releases waiters; unfinished
daemon reads cannot publish or block process exit. See the
refresh contract. Leave the provider unset for
local evaluation without background credential acquisition.
| Source | Purpose |
|---|---|
| function_app.py | Typed request orchestration and direct provider selection |
| src/config.py | Shared deployment settings |
| src/models.py | Typed Entra payload, delivery context, provider request and result dataclasses |
| src/jwe.py | Pinned JWE decryption and typed delivery-context conversion |
| src/provider.py | Shared HTTPS transport, timeout handling and endpoint status mapping |
| src/credentials.py | CredentialTokenService, ApiKeyCache and AccessTokenCache |
| src/otp_log.py | Fixed standard-logging event definitions and immutable request context |
| src/providers/ | Provider-owned credentials, requests and response mapping |
| src/secrets.py | Key Vault transport; bundle caching belongs to ApiKeyCache |
Add a provider by subclassing PhoneProviderBase, declaring its credential specification, and
implementing build_request and map_response. Return ProviderResult with the coarse endpoint
outcome and a fixed safe failure classification. Raw provider JSON remains local to the provider.
See production limitations before production use.