From f5e0052c1f0e08206e84106f2b57d586ee3b91f7 Mon Sep 17 00:00:00 2001 From: Hou Chi Chan Date: Thu, 15 Oct 2026 15:34:25 -0700 Subject: [PATCH 1/5] docs: simplify API and provider guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe53050-0d2b-4d05-bcc1-20f5b611b665 --- README.md | 2 +- TECHNICAL.md | 103 +--- docs/BRING-YOUR-OWN-PROVIDER.md | 384 ++++++++----- docs/CONTRACT.md | 770 +++++++++----------------- docs/GETTING-STARTED.md | 2 +- docs/MONITORING.md | 2 +- docs/MULTI-PROVIDER-IMPLEMENTATION.md | 10 +- docs/ONBOARDING.md | 5 +- docs/README.md | 2 +- dotnet/README.md | 8 +- javascript/README.md | 10 +- python/README.md | 10 +- setup/docs/Troubleshooting.md | 2 +- 13 files changed, 554 insertions(+), 756 deletions(-) diff --git a/README.md b/README.md index b4735f4..b913c14 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Follow the **[five-step setup guide](docs/GETTING-STARTED.md)** in order: 4. [Connect and test](docs/GETTING-STARTED.md#4-connect-your-provider-and-test): configure provider access and run approved checks. 5. [Activate and hand over](docs/GETTING-STARTED.md#5-activate-and-hand-over): activate after validation and assign owners. -Before live use, review [production readiness](docs/CONTRACT.md#production-limitations). +Before live use, review [production readiness](docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness). ## Guidance diff --git a/TECHNICAL.md b/TECHNICAL.md index 9cc60b3..3b20c1c 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -20,7 +20,7 @@ Function decrypts the request, and the selected adapter submits a live message. provider acceptance, not confirmed delivery. East US is illustrative. API-key integrations use provider secrets; OAuth integrations use -managed-identity token exchange. See [credential caching](docs/CONTRACT.md#credential-caching-and-refresh) +managed-identity token exchange. See [credential caching](docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) and [runtime logs](docs/MONITORING.md#runtime-specific-log-discovery). An authorized **evaluation request (`mode: 2`)** returns the nonce without sending a message. @@ -145,24 +145,9 @@ For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript, | `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. | | `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_SMS_ENDPOINT` | Required for SMS delivery | Provider-approved HTTPS SMS endpoint. Guided setup writes the complete send URL from the selected regional profile. | -| `EPP_PROVIDER_VOICE_ENDPOINT` | Required for voice delivery | Provider-approved HTTPS voice endpoint, which can differ from the SMS endpoint. Guided setup writes the complete send URL. | -| `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 Infobip, Sinch, or Telesign; `oauth` for Soprano. Bundled adapters do not all have guided setup profiles. | -| `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 and outbound user-assigned managed identity 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_KEY_VAULT_CACHE_ENABLED` | Optional | `true` enables provider API-key bundle caching; `false` reads the bundle for each live request. Unset defaults to `true`. | -| `EPP_ACCESS_TOKEN_CACHE_ENABLED` | Optional | `true` enables OAuth credential caching; `false` uses request-scoped MI/client-assertion credentials. Unset defaults to `true`. | -| `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. | -| `AZURE_CLIENT_ID` | Optional | User-assigned managed identity's client ID for Key Vault. Leave unset for system-assigned identity. | - -The request's channel-specific endpoint is required. A missing, empty, or invalid endpoint fails -closed; there is no shared or other-channel endpoint fallback. Telesign and Soprano -use complete URLs unchanged. Infobip and Sinch retain their adapter-specific base-URL behavior; -see [endpoint selection](docs/CONTRACT.md#default-provider-and-configuration-readers). + +See [provider settings](docs/BRING-YOUR-OWN-PROVIDER.md#provider-settings) for provider IDs, +endpoints, authentication, credentials, and caching. 1. **Locally:** create private local settings beside the chosen runtime's host file, following its [JavaScript](javascript/README.md#environment-configuration), [Python](python/README.md#environment-configuration) @@ -170,24 +155,11 @@ see [endpoint selection](docs/CONTRACT.md#default-provider-and-configuration-rea 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. For Telesign, store provider API credentials in Key Vault using the exact manifest names. For - Soprano, configure provider consent plus the profile's tenant/scope and outbound managed-identity - federation; the Function stores no Soprano client secret. - -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 -login; ordinary local machines have no managed-identity endpoint. Use offline tests or loopback-only -evaluation locally, or an explicitly injected test resolver for integration work. Never commit local -settings, keys or test credentials. - -For cache values, failure behavior, and runtime differences, use the -[credential caching contract](docs/CONTRACT.md#credential-caching-and-refresh). -JavaScript/Python poll; .NET refreshes on cache misses. Setup writes both switches as `false` for FC1 -or `true` for EP1; deploy a supporting package and restart after changes. -Evaluation skips provider work, but cached workers can independently acquire credentials without -sending an OTP. Leave `EPP_PROVIDER_NAME` unset or disable its cache for local evaluation without -credential acquisition. Provider caching is separate from platform-managed identity caching and -decryption-key references. +Evaluation needs the decryption key but not provider settings. Use offline tests or loopback-only +evaluation locally. Never commit local settings, keys, or test credentials. + +For cache behavior, see +[credential caching](docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). 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.azure.net/secrets//)` @@ -206,60 +178,13 @@ there is no deployment-level channel restriction. The legacy `EPP_PROVIDER_CHANN ## Provider-specific integration details -Use the [credential and deployment-support table](docs/ONBOARDING.md#provider-credential-names) -for your selected adapter. The [Soprano OAuth and voice contract](docs/CONTRACT.md#soprano-provider-jwt) -and [Telesign EPP contract](docs/CONTRACT.md#telesign-epp-integration) describe different API requirements, -not a provider ranking. The wire example below illustrates Telesign's format only. - -### Telesign EPP - -The `telesign` adapter sends its JSON contract to the complete SMS or voice URL selected by the -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 provider-token authentication are not -implemented. The incoming caller's Authorization header is never forwarded. - -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": "sms" }], - "correlation_id": "unique-string-123" -} -``` +Use the [credential and deployment-support table](docs/ONBOARDING.md#provider-credential-names) +for onboarding. Implementation details are in: -`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`; the leading `+` is preserved. SMS passes the complete -`message` unchanged as `message.text`, including whitespace. For Voice, each standalone six-digit -number is rendered with comma-separated digits. The complete paced message is sent twice with one -separating space. Telesign performs text-to-speech for Voice; no -separate speech object is needed. -A nonblank string `locale` becomes `message.language`; otherwise language is omitted. The envelope -channel selects the single `sms` or `voice` entry. `correlation_id` uses a nonempty string request -correlation ID, falling back to the message ID for absent, empty, or non-string values. Reserved -`account_lifecycle_event` and `originating_ip` fields are -not sent; no client-IP inference or account-event default is applied. `TELESIGN_VOICE` and the -legacy sender/form fields no longer affect this adapter. - -Telesign's API supports `X-Shutter-Mode: true` for direct provider tests. The Function deliberately -omits that header on live sends and does not forward it from incoming requests. Use the existing -`mode: 2` evaluation path for Function tests without delivery: it skips provider HTTP and credential -lookup entirely, rather than invoking Telesign shutter mode. - -Responses normalize `reference_id` and `status.code`/`status.description` internally. Provider -metadata is not exposed in the public nonce response. [Application logs](docs/LOGGING.md#safe-fields-and-identifiers) -include the provider HTTP status and mapped status/outcome. They also include a length-limited -provider reference ID as `providerMessageId` for support lookup. They exclude the raw response -and description. -Existing numeric success codes remain unchanged (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 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. +- [Soprano](docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) +- [Telesign](docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration) ## Security diff --git a/docs/BRING-YOUR-OWN-PROVIDER.md b/docs/BRING-YOUR-OWN-PROVIDER.md index 67051f5..f089bc3 100644 --- a/docs/BRING-YOUR-OWN-PROVIDER.md +++ b/docs/BRING-YOUR-OWN-PROVIDER.md @@ -5,163 +5,288 @@ [Documentation](README.md) | [Exact HTTP contract](CONTRACT.md) | [Runtime guides](README.md#develop) Connect a phone service to the **External Phone Provider (EPP)** contract. **Microsoft Entra ID** -sends an encrypted delivery request; the endpoint verifies, decrypts, evaluates, or sends it. +sends an encrypted delivery request. The endpoint verifies, decrypts, evaluates, or sends it. Confirm [eligibility, testing, and activation](ONBOARDING.md#before-purchasing-or-deploying) before deployment. -Follow [caller verification](#1-verify-the-caller), [decryption](#2-read-and-decrypt-the-request), -[evaluation/live handling](#3-evaluate-or-send), and [responses](#4-respond-to-entra-id). -Then [register your integration](#where-to-add-your-integration) and [validate before activation](#before-activation). +Read the [API contract](CONTRACT.md), then [add your integration](#where-to-add-your-integration) +and [validate it](#before-activation). + + + + + + -## 1. Verify the caller +## Understand the request -Entra ID sends **`POST /api/SendOtp` over HTTPS** with a signed bearer token. **Easy Auth** -authorizes the caller before the anonymous Function trigger runs. +The [Send OTP API reference](CONTRACT.md) is the single source for: -Require HTTPS, return `401` for unauthenticated requests, and restrict access to the trusted -issuer, audience, and Microsoft caller. Follow the -[trust checklist](ONBOARDING.md#manual-deployment-trust-checklist). +- Authentication and headers. +- Envelope and JWE fields. +- Evaluation and live modes. +- Success and error responses. +- HTTP status codes. +- Runtime parsing differences. -**Encryption is not caller authentication.** A JWE, nonce, `tenantId`, or tracing header does -not prove that Entra ID sent the request. +Provider code starts after caller authentication, envelope validation, and decryption. It receives +validated delivery data and returns a normalized provider result. -## 2. Read and decrypt the request +Keep these boundaries: -The JSON body is an envelope with these fields. Use the forms below across all sample runtimes: +- Easy Auth verifies the caller. +- The shared handler validates and decrypts the request. +- The provider adapter builds the outbound request and interprets the response. +- The shared handler creates the response to Microsoft Entra ID. -| Field | Required | Value | -| --- | --- | --- | -| `type` | Yes | String, exactly `microsoft.mfa.otpDeliver.v1`. | -| `channel` | Yes | Integer `1` or string `"sms"`; integer `2` or string `"voice"`. | -| `mode` | Yes | Integer `1` or string `"live"`; integer `2` or string `"evaluation"`. | -| `encryptedDeliveryContext` | Yes | Nonblank string containing the compact JWE. | -| `tenantId` | No | String containing routing metadata, not a source of trust or service selection. | -| `correlationId` | No | Nonempty string for sign-in tracing. | -| `ttlSeconds` | No | Positive JSON integer up to `2147483647`, or omit it. Use `60`, not `"60"`, `60.0`, `6e1`, or null. | +Do not forward the incoming bearer token. Do not return raw provider JSON. Echo the nonce only +after evaluation or recognized provider acceptance. -Channel and mode names are case-insensitive. TTL validation does not enforce passcode expiry -or a delivery deadline. +## Where to add your integration - +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 the +incoming caller or write the HTTP response to Entra ID. -Decrypt `encryptedDeliveryContext` using only **`RSA-OAEP-256`** for key wrapping and **`A256GCM`** -for content encryption. Accept exactly five nonempty compact-JWE segments, at most **16,384 -characters**. Pass the original JWE, including its protected-header bytes, to a JOSE library. -Verify the authentication tag before using the decrypted UTF-8 JSON. + +### Provider request and response mapping -The sample uses one RSA private key in `EPP_DECRYPTION_KEY_PEM` as text or base64 PEM. In Azure, -use a managed-identity-resolved Key Vault reference. +Each provider implementation owns: -The JWE `kid` does not select a key. `EPP_ENCRYPTION_KEY_ID` only warns after decryption. -See the [certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle). +- Its fixed provider ID and authentication mode. +- Its required credentials and settings. +- SMS and voice request construction. +- Response parsing and accepted-status rules. +- Safe failure classification. -The decrypted delivery context contains: +The shared transport owns: -| Field | Required | Value and handling | -| --- | --- | --- | -| `nonce` | Yes | Nonblank string. Keep it unchanged for the response. It is not replay protection. | -| `phoneNumber` | Yes | Nonblank string, expected in E.164 form. Full E.164 validation is not implemented. | -| `message` | Yes | Nonblank string containing the localized message and passcode. Preserve them; do not generate a new code. | -| `locale` | No | String for voice selection where supported; no shared locale validation. | -| `extension` | No | Office-voice data; no shared type validation or forwarding. | -| `riskContext` | No | Contextual JSON data; no shared schema validation, risk evaluation, or forwarding. | -| `textToVoice` | No | Legacy speech data; no portable schema or shared forwarding. | +- HTTPS URL validation. +- Disabled redirects. +- The configured provider timeout. +- Response-body reading. +- Network and timeout errors. -Treat optional fields as available only when the selected integration supports them. -For parser and wrong-type handling, see [runtime binding differences](CONTRACT.md#runtime-binding-differences). +Return a normalized result to the handler. Do not return raw provider JSON to Microsoft Entra ID +or write it to logs. The shared handler applies the +[HTTP status mapping](CONTRACT.md#2-outcome--http-status-mapping). - +A provider `2xx` response is not enough unless the adapter recognizes its status as accepted. +Acceptance is not recipient delivery. +The shared outcomes are `Continue`, `Block`, and `Fail`. There is no `StepUp` or `409` mapping. +Use the existing safe failure reasons: `provider_http_error`, `provider_rejected`, +`unrecognized_provider_status`, `missing_provider_message_id`, and `invalid_provider_json`. + +JavaScript and .NET pass cancellation through response reading. Python uses connect and read +inactivity timeouts. + + +### Provider settings + +#### Provider and endpoint + +- **`EPP_PROVIDER_NAME`** is the fixed ID of the registered adapter. +- **`EPP_PROVIDER_SMS_ENDPOINT`** is the required SMS URL for a live SMS request. +- **`EPP_PROVIDER_VOICE_ENDPOINT`** is the required voice URL for a live voice request. +- **`EPP_PROVIDER_ENDPOINT_REGION`** is the `global` or `eu` routing label from guided setup. +- **`EPP_PROVIDER_TIMEOUT_MS`** is the provider timeout in milliseconds. The default is `1500`. + The maximum is `2500`. +- **`EPP_PROVIDER_ACCOUNT_NAME`** is the sender or account value when required. + +#### Authentication + +- **`EPP_PROVIDER_AUTH_MODE`** is `apiKey` or `oauth`, matching the adapter. +- **`KEY_VAULT_URL`** identifies the vault containing API-key credentials. +- **`AZURE_CLIENT_ID`** selects a user-assigned identity for Key Vault when needed. +- **`EPP_PROVIDER_TENANT_ID`** identifies the provider tenant for OAuth. +- **`EPP_PROVIDER_SCOPE`** is the provider OAuth scope. +- **`EPP_OUTBOUND_CLIENT_ID`** identifies the client application used for OAuth exchange. +- **`EPP_OUTBOUND_MI_CLIENT_ID`** identifies the user-assigned identity used for OAuth exchange. + +#### Caching -## 3. Evaluate or send +- **`EPP_KEY_VAULT_CACHE_ENABLED`** controls API-key credential caching. +- **`EPP_ACCESS_TOKEN_CACHE_ENABLED`** controls OAuth credential caching. -**Evaluation (`mode: 2` or `"evaluation"`):** authenticate, validate, decrypt, and return success -without service selection, credential lookup, outbound HTTP, or delivery. +Each live request uses the endpoint for its selected channel. There is no shared endpoint fallback. +Endpoints must be absolute HTTPS URLs with a hostname, valid port, no user information, and no +fragment. Redirects are not followed. -**Live (`mode: 1` or `"live"`):** map SMS or voice to your service while preserving the -destination, message, and passcode. **Never forward the incoming bearer token**. +Settings select registered code. They do not install an adapter. Evaluation does not require a +provider name, endpoint, or provider credential. -Return success after the integration recognizes provider acceptance. A queued or pending result -counts only when the integration defines it as accepted. Map unknown, incomplete, or failed HTTP -responses to an error, even when the response body looks successful. Use the shared response -behavior below instead of returning raw service JSON. +There is no default provider or automatic failover. The request body does not select a provider. +Telesign and Soprano use the selected URL unchanged. Infobip and Sinch append their API paths to +the selected base URL. The request channel selects the SMS or voice endpoint. +The legacy `EPP_PROVIDER_CHANNEL` setting is not used. -The sample uses one configured service per deployment. Follow -[Where to add your integration](#where-to-add-your-integration) for the source files, -registration, settings, packaging, and custom deployment steps. + +### Authentication and credential caching -## 4. Respond to Entra ID +Use managed identity to read provider credentials from Key Vault. Keep secrets out of code, +settings, logs, and documentation. +The default runtime resolver uses `ManagedIdentityCredential`, not the developer's CLI identity. -Return **HTTP `200` with a JSON object containing exactly these fields** after a successful -evaluation or recognized live acceptance: +For OAuth, exchange the outbound managed-identity assertion for the provider token. Do not use a +client-secret fallback and never forward the incoming Microsoft Entra ID token. -| Field | Value | +Soprano uses `EPP_PROVIDER_AUTH_MODE=oauth`. With +`EPP_ACCESS_TOKEN_CACHE_ENABLED=true`, the runtime reuses its credential and SDK token caches. + +Cache settings accept trimmed, case-insensitive `true` or `false`. Unset values default to `true`. +Other values fail live credential acquisition without blocking evaluation. Restart workers after +a change. + +| Runtime | Enabled cache behavior | | --- | --- | -| `nonce` | The original decrypted nonce string. | -| `correlationId` | The request body's correlation string, then `x-ms-correlation-id` as fallback, then a generated request ID. | -| `providerStatus` | The literal string `"accepted"`, also used for evaluation. | - -Correlation header sanitization and empty-value handling differ by runtime; see the -[binding reference](CONTRACT.md#runtime-binding-differences). `x-ms-client-request-id` supplies -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. 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 -available. Do not return raw exceptions or service responses. Authentication and host-binding -errors can occur before the handler and need not use this JSON shape. Use the -[error reference](CONTRACT.md#epp-response-json) and -[HTTP status mapping](CONTRACT.md#2-outcome--http-status-mapping) for the exact failure response. +| JavaScript | Warms at startup and polls every 30 seconds. OAuth uses `getToken`. | +| Python | Uses the same polling model. OAuth uses `get_token_info` when available, then `get_token`. | +| .NET | Warms once and refreshes on the next cache miss through `GetOrCreateAsync`. It has no polling loop. | -## Where to add your integration +With caching disabled, each live request obtains credentials for that request. There is no startup +preparation, polling, or cross-request reuse. Evaluation still skips provider credential work. +The platform's decryption-key reference is separate from these provider caches. -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 the -incoming caller or write the HTTP response to Entra ID. +Credential acquisition uses a 2.5-second budget in each runtime. That budget is not the total +Function deadline. Failed credential acquisition never sends an OTP. + +### Built-in provider notes + + +#### Soprano + +Soprano uses OAuth client-assertion exchange with: + +- `EPP_PROVIDER_TENANT_ID` +- `EPP_PROVIDER_SCOPE` +- `EPP_OUTBOUND_CLIENT_ID` +- `EPP_OUTBOUND_MI_CLIENT_ID` + +The endpoint app and outbound identity share a home tenant. The app must be multitenant and +authorized in the provider tenant. The federated credential uses the identity principal ID, +home-tenant v2 issuer, and `api://AzureADTokenExchange` audience. + +The managed identity first requests `api://AzureADTokenExchange/.default`. The selected provider +scope is used for the final application token. The Function sends that token to the +`/messages/omnimsg` API and does not store a Soprano client secret. + +Only the final provider token is sent. The Function does not forward the managed-identity +assertion or incoming Microsoft Entra ID token. Missing settings, token failures, blank tokens, and +tokens with 30 seconds or less remaining fail before provider HTTP. There is no API-key fallback or +provider retry. The Function treats the token as opaque. The provider validates its claims. + +The selected SMS or voice URL is used unchanged. + +For voice, the adapter: + +1. Finds the first six consecutive digits in `message`, preserving leading zeros. +2. Splits the text into `beforePasswordText`, `password`, and `afterPasswordText`. +3. Uses the request locale or `en-US`. +4. Sends `gender: 1` and `loop: 2` in `voice.text2voice`. + +A message without six consecutive digits fails before provider HTTP. SMS forwards `message` +unchanged. Voice does not send a top-level `text` field. + + +#### Telesign + +Telesign uses the complete configured SMS or voice URL. It sends: + +- `recipient.phone_number` +- `message.text` +- Optional `message.language` +- One `channels[].channel` +- `correlation_id` + +Responses use `reference_id`, `status.code`, and `status.description`. Only safe normalized fields +are logged. + +`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`, including the leading `+`. A nonblank locale +becomes `message.language`. `correlation_id` uses the request correlation ID or the generated +message ID. + +SMS forwards the message unchanged. Voice separates standalone six-digit runs with commas and +repeats the full message twice. + +Basic authentication uses: + +```text +base64(UTF8(customer-id:api-key)) +``` + +Set the header to `Authorization: Basic {encoded-value}`. Provider documentation can abbreviate +this as `Basic YOUR_API_KEY`. Do not send that text literally. + +Read `telesign-customer-id` and `telesign-api-key` from Key Vault. Digest and provider-token +authentication are not implemented. + +Recognized success codes are: + +- SMS: `200`, `203`, `290`, `291`, `292` +- Voice: `100`, `101`, `102`, `103` +- Either channel: `3001` on a successful HTTP response + +Unknown or malformed statuses fail closed. `3001` means the provider accepted the operation. It +does not confirm handset receipt or completed audio. + +The adapter does not send `account_lifecycle_event`, `originating_ip`, or `X-Shutter-Mode: true`. +The legacy `TELESIGN_VOICE` setting has no effect. +Use Function evaluation mode for a no-send endpoint test. ### Add and register the provider -Choose a fixed lowercase provider ID, then follow the row for your runtime. - -| Runtime | Create your module or class here | Register it here | Add tests here | -| --- | --- | --- | --- | -| JavaScript | [javascript/src/functions/providers/](../javascript/src/functions/providers/) | [providers/index.js](../javascript/src/functions/providers/index.js): import the module, add its fixed ID to `selectProvider`, and export it. | [javascript/test/](../javascript/test/) | -| C# | [dotnet/Src/Providers/](../dotnet/Src/Providers/) | [Program.cs](../dotnet/Program.cs): register the class as a singleton `PhoneProviderBase`. The handler selects it by `Name`. | [dotnet/tests/](../dotnet/tests/) | -| Python | [python/src/providers/](../python/src/providers/) | [function_app.py](../python/function_app.py): import the class and add an instance to the tuple that builds `_providers`. | [python/tests/](../python/tests/) | - -Implement the hooks for your chosen runtime: - -- **JavaScript:** export `name`, `authenticationMode`, `credentialSpec`, `createRequest`, and - `interpretResponse`. Return the shared [ProviderResult](../javascript/src/functions/providerResult.js). - [SendOtp.js](../javascript/src/functions/SendOtp.js) passes delivery data, credentials, and `env` - to `createRequest`; leave sending to its shared [transport](../javascript/src/functions/providerTransport.js). -- **C#:** derive from [PhoneProviderBase](../dotnet/Src/PhoneProviderBase.cs). Implement `Name`, - `AuthenticationMode`, `FetchCredentialsAsync`, and `SendOtpAsync`. Use the base `SendJsonAsync` - transport and return [ProviderResult](../dotnet/Src/Models.cs). Read extra send settings through - the supplied `IEnv`; use injected [ISecretResolver](../dotnet/Src/ISecretResolver.cs) for vault secrets. - Return credentials with a valid `ExpiresOn` and honor the acquisition cancellation token. -- **Python:** derive from [PhoneProviderBase](../python/src/provider.py). Supply `name`, - `authentication_mode`, and `credential_spec`; implement `build_request` and `map_response`. - Return the base module's `OutboundRequest` and the shared [ProviderResult](../python/src/models.py). - Use the supplied `env` for extra settings and keep the base `send_otp` transport. +Choose a fixed lowercase provider ID. + +#### JavaScript + +1. Add the module under [providers/](../javascript/src/functions/providers/). +2. Import it in [providers/index.js](../javascript/src/functions/providers/index.js), add its ID to + `selectProvider`, and export it. +3. Export `name`, `authenticationMode`, `credentialSpec`, `createRequest`, and `interpretResponse`. +4. Return [ProviderResult](../javascript/src/functions/providerResult.js). +5. Add tests under [javascript/test/](../javascript/test/). + +`createRequest` receives the delivery data, credentials, and `env`. Keep sending in the shared +[transport](../javascript/src/functions/providerTransport.js). + +#### C# + +1. Add the class under [Providers/](../dotnet/Src/Providers/). +2. Register it in [Program.cs](../dotnet/Program.cs) as a singleton `PhoneProviderBase`. +3. Implement `Name`, `AuthenticationMode`, `FetchCredentialsAsync`, and `SendOtpAsync`. +4. Use `SendJsonAsync` and return [ProviderResult](../dotnet/Src/Models.cs). +5. Add tests under [dotnet/tests/](../dotnet/tests/). + +Use `IEnv` for settings and [ISecretResolver](../dotnet/Src/ISecretResolver.cs) for vault secrets. +Return credentials with a valid `ExpiresOn` and honor cancellation. + +#### Python + +1. Add the class under [providers/](../python/src/providers/). +2. Add an instance to the `_providers` tuple in [function_app.py](../python/function_app.py). +3. Derive from [PhoneProviderBase](../python/src/provider.py). +4. Implement `name`, `authentication_mode`, `credential_spec`, `build_request`, and `map_response`. +5. Return `OutboundRequest` and [ProviderResult](../python/src/models.py). +6. Add tests under [python/tests/](../python/tests/). + +Use the supplied `env` for extra settings. Keep sending in the base `send_otp` transport. Your hooks own service-specific mapping, outbound authentication, and recognition of acceptance or failure. Keep credential caching in the existing services: [JavaScript credentials.js](../javascript/src/functions/credentials.js), [C# CredentialTokenService](../dotnet/Src/CredentialTokenService.cs), and [Python credentials.py](../python/src/credentials.py). JavaScript/Python resolve the provider's -credential specification; C# calls `FetchCredentialsAsync`. Do not add a separate credential cache +credential specification. C# calls `FetchCredentialsAsync`. Do not add a separate credential cache or send-retry loop. Keep service secrets in Key Vault, not in settings or source. +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). + ### Implementation steps @@ -174,14 +299,14 @@ or send-retry loop. Keep service secrets in Key Vault, not in settings or source for the channels you support, plus the credential and extra settings your code reads. Keep these aligned with [JavaScript config.js](../javascript/src/functions/config.js), [C# AppConfig](../dotnet/Src/AppConfig.cs), or [Python config.py](../python/src/config.py) and the - [configuration reference](CONTRACT.md#4-configuration-app-settings--env). - **Settings select registered code; they do not create or register an adapter.** + [provider settings](#provider-settings). + **Settings select registered code. They do not create or register an adapter.** 3. Add tests in the directory above for registration, request mapping, missing credentials, recognized acceptance, rejection, unknown responses, and timeouts on each supported channel. Check that evaluation sends nothing, live success waits for acceptance, and failures omit the - nonce. Use fake credentials and service responses; run your runtime's tests and build. + nonce. Use fake credentials and service responses. Run your runtime's tests and build. 4. [Package your changed source](../TECHNICAL.md#build-zips-locally), not an unchanged public - release. C# needs publish output; Python needs the documented Linux remote build. + release. C# needs publish output. Python needs the documented Linux remote build. Follow [manual deployment](ONBOARDING.md#optional-local-development-and-manual-deployment) to deploy the custom package and apply cloud settings and vault permissions separately. Keep the [Easy Auth trust checks](ONBOARDING.md#manual-deployment-trust-checklist), then @@ -192,17 +317,28 @@ or send-retry loop. Keep service secrets in Key Vault, not in settings or source ## Before activation + + Run [authorized endpoint checks](ONBOARDING.md#validate-the-deployed-endpoint): rejected callers, invalid or tampered payloads, evaluation without a send, and controlled live tests for each channel. Check handset receipt or voice audio separately from acceptance. For local tests, use test keys -and fake service responses on loopback only, without tunnels; Core Tools has no Easy Auth. +and fake service responses on loopback only, without tunnels. Core Tools has no Easy Auth. + +The sample completes the request after provider acceptance. Plan any additional production +capabilities your design needs: + +- Durable queues or background delivery. +- Duplicate-send protection and safe retries. +- End-to-end timing for cold starts, Key Vault, and provider calls. +- Overlapping encryption-key rotation. +- E.164 and body-size validation. +- Provider delivery confirmation. +- Voice locale, passcode, and audio validation. -The sample completes the request after provider acceptance. A live design can add early -acknowledgement, background delivery, a durable queue, retries, or duplicate-send protection. -Define the timing and owner for each feature. The outbound timeout covers the provider call, -not the whole request. Check a timed-out send before retrying because the provider may already -have accepted it. +The outbound timeout covers only the provider call. Check a timed-out send before retrying because +the provider may already have accepted it. Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, and raw service responses out of logs. Disable body tracing in the application, platform, and proxies. -Review the [production-readiness considerations](CONTRACT.md#production-limitations) before activation. +Complete setup certification, provider account checks, monitoring, renewal, rollback, and the +approved Microsoft activation procedure before live use. diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index 4e0979d..cc8e803 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -1,541 +1,277 @@ -# HTTP contract +# Send OTP API + -[Documentation](README.md) | [Implementation walkthrough](BRING-YOUR-OWN-PROVIDER.md) | [Settings and packages](../TECHNICAL.md) +[Documentation](README.md) | [Build a provider integration](BRING-YOUR-OWN-PROVIDER.md) | [Settings and packages](../TECHNICAL.md) -Shared contract for [JavaScript](../javascript/), [Python](../python/), and [.NET](../dotnet/). +Use this API to receive an encrypted SMS or voice request from **Microsoft Entra ID**. -Go to [request/response](#1-http-api), [HTTP outcomes](#2-outcome--http-status-mapping), -[provider contract](#3-provider-contract), [settings](#4-configuration-app-settings--env), -[caching](#credential-caching-and-refresh), [required behaviors](#5-required-behaviors), -[logs](#application-logs), or [tests](#6-lightweight-tests). +| | | +| --- | --- | +| **Method** | `POST` | +| **Path** | `/api/SendOtp` | +| **Authentication** | Easy Auth | +| **Function trigger** | `authLevel: anonymous` | +| **Content type** | `application/json` | +| **Success** | `200 OK` with the decrypted nonce | -> **Naming:** EPP means **External Phone Provider**. App settings keep the `EPP_` prefix; -> documentation names do not change external fields or `microsoft.mfa.otpDeliver.v1`. +## HTTP request -Each deployment selects one provider. Provider-specific API behavior stays inside its implementation. + -The [Front Door design](FRONTDOOR.md) routes this API to regional origins. Its readiness endpoint -is separate and never delivers OTPs. +```http +POST https:///api/SendOtp +Content-Type: application/json +Authorization: Bearer {access-token} +``` + +Easy Auth validates the bearer token before the Function runs. Configure the trusted issuer, +endpoint audience, and authorized Microsoft caller by following the +[Easy Auth checklist](ONBOARDING.md#manual-deployment-trust-checklist). ---- +The JWE protects the request data. It does not authenticate the caller. -## 1. HTTP API +Easy Auth must use `requireAuthentication=true`, `unauthenticatedClientAction=Return401`, +`requireHttps=true`, the endpoint audience in `allowedAudiences`, and a nonempty Microsoft caller +allowlist. -**Endpoint:** `POST /api/SendOtp` (`authLevel: anonymous`). **Easy Auth is the caller-authentication -boundary before the handler**. The phone number and message are encrypted inside a JWE; the -cleartext envelope carries routing data. +## Request headers -### Request headers +| Header | Use | +| --- | --- | +| `Authorization` | **Required.** Bearer token consumed by Easy Auth. The Function does not parse or forward it. | +| `Content-Type` | **Required.** Must be `application/json`. | +| `x-ms-correlation-id` | Optional correlation fallback when the body does not contain `correlationId`. | +| `x-ms-client-request-id` | Optional per-attempt identifier used internally as `messageId`. | +| `User-Agent` | Optional. Not interpreted or logged by the sample. | -| Header | Notes | -|--------|-------| -| `Authorization` | consumed by platform authentication, not parsed or echoed by handler | -| `User-Agent` | caller-supplied identifier; not interpreted or logged | -| `x-ms-correlation-id` | tracing only; fallback for envelope `correlationId`, not authentication | -| `x-ms-client-request-id` | per-attempt tracing id (used as `messageId`), not authentication | +Forwarded platform headers, including `x-ms-client-principal`, are context only. -Forwarded headers such as `x-ms-client-principal` provide context, not authentication. The handler -never forwards `Authorization` to the provider. +## Request body -### EPP request body - -| Field | Required | Notes | -|-------|----------|-------| -| `type` | yes | exactly `microsoft.mfa.otpDeliver.v1`; unknown versions are rejected | -| `tenantId` | no | opaque request routing metadata; never selects the trusted issuer, signing keys or provider | -| `correlationId` | no | sign-in tracing metadata | -| `channel` | yes | request delivery channel: `1`/`sms` or `2`/`voice`; not deployment configuration | -| `mode` | yes | request delivery mode: `1`/`live` or `2`/`evaluation`; evaluation does not deliver | -| `ttlSeconds` | no | positive JSON integer, at most `2147483647`; omit when absent. Null is not portable; booleans, strings, fractions and nonpositive values are invalid. Use canonical integer notation (`60`, not `60.0` or `6e1`) across runtimes | -| `encryptedDeliveryContext` | yes | JWE compact serialization (see below) | - -Canonical integer notation is a caller requirement, not a portable raw-JSON-token check: JavaScript's -JSON parser normalizes `60.0` and `6e1` to `60`, while Python/.NET reject those representations here. -Always send `60` to obtain the same result across runtimes; no custom JSON tokenizer is used. - -Handler validation failures for unknown `type`, invalid `ttlSeconds`, unsupported `channel` or -`mode`, or missing/empty `encryptedDeliveryContext` return `400`. Arrays, objects and booleans -are not channel/mode values. Some invalid inputs fail .NET host binding before the handler; -see [runtime binding differences](#runtime-binding-differences). - -These are request data, not settings to provision. The TTL check validates the supplied value; it -does not verify passcode expiry or implement a delivery deadline. Deployment trust comes only from -the required platform authentication and caller allowlist, not the body or tracing headers. - -### `encryptedDeliveryContext` (JWE) - -Alg: **RSA-OAEP-256** (CEK wrap) + **A256GCM** (content). The JOSE protected header carries `kid`; -this sample uses the single configured RSA private key (`EPP_DECRYPTION_KEY_PEM`, a Key Vault -reference in Azure), not a multi-key lookup. The compact JWE must have **exactly five non-empty -segments** and at most **16,384 characters**; `alg`/`enc` are pinned (only `RSA-OAEP-256` + `A256GCM` accepted) and the AES-GCM auth tag is -verified before any plaintext is used. Decrypted plaintext = `DeliveryContext`: - -The original compact JWE is passed unchanged to the JOSE library. Parsing header fields for the -advisory key-ID check must not replace the original protected-header bytes used for authentication. - -`EPP_DECRYPTION_KEY_PEM` accepts a private-key PEM alone or a PEM certificate bundle containing the -private key, in plain text or base64 form. Guided setup issues a certificate inside Key Vault or -reuses a selected certificate version. It pins the PEM backing-secret reference and keeps exactly -one approved encryption credential on the application. Certificate synchronization -is manual; Key Vault renewal does not automatically update Entra or select another decryption key. See the -[certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle). - -All three HTTP-handler suites use [shared policy cases](../tests/fixtures/contract.json): the allowed -pair succeeds, while `RSA-OAEP`, `A128GCM` and `A256CBC-HS512` alternatives return `400 decryption_failed` -without provider I/O. Decryption uses the same policy before live/evaluation branching, so the matrix -runs once per language. Tag tampering and original-header-byte tests remain. - -| Field | Required | Notes | -|-------|----------|-------| -| `nonce` | yes | value the endpoint MUST echo to prove decryption | -| `phoneNumber` | yes | caller supplies an E.164 string; full E.164 validation is an implementation gap | -| `message` | yes | fully rendered, localized text containing the passcode; text-message providers forward it unchanged, Soprano voice extracts the first six consecutive digits, and Telesign voice paces standalone six-digit numeric runs and repeats the full message twice | -| `extension` | no | office-voice contract field; not currently forwarded by the shared dispatch model | -| `locale` | no | voice selection input where supported by the selected provider | -| `riskContext` | no | contextual request data; no risk-policy evaluation is implemented here | -| `textToVoice` | no | legacy structured speech input; current Soprano implementations ignore it | - -Decryption failure → `400`. Missing `nonce` / `phoneNumber` / `message` → `400`. - -For Soprano live voice, the provider finds the first six consecutive digits in `message`, preserving -leading zeros, and splits the rendered text into `beforePasswordText`, `password`, and -`afterPasswordText`. It uses the request's nonblank `locale` as `language`, falling back to `en-US`, and sends -fixed `gender: 1` and `loop: 2`. The resulting object is sent as `voice.text2voice` without a top-level -`text` field. A message without a six-digit sequence fails closed before provider HTTP. No additional -environment settings are required. Soprano SMS continues to forward `message` unchanged, and -evaluation continues to skip provider-specific validation and I/O. -Soprano uses OAuth client-assertion exchange. The outbound user-assigned managed identity obtains an -`api://AzureADTokenExchange/.default` assertion for the existing multitenant application, which then -requests the configured provider scope. Existing platform caller authentication is unchanged. - -### Soprano provider JWT - -The Function obtains one provider token through the shared OAuth credential resolver using the -setup-generated `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE`, `EPP_OUTBOUND_CLIENT_ID`, and -`EPP_OUTBOUND_MI_CLIENT_ID`. `EPP_PROVIDER_AUTH_MODE=oauth` matches the Soprano provider. -The request channel selects the required `EPP_PROVIDER_SMS_ENDPOINT` or `EPP_PROVIDER_VOICE_ENDPOINT`. -The selected complete send URL is not modified by the provider. -The calling app registration and outbound user-assigned identity must share a home tenant; the -calling app must be multitenant and provisioned/authorized in the provider tenant. The app's -federated credential trusts the identity's principal ID, home-tenant v2 issuer, and -`api://AzureADTokenExchange` audience. Key Vault identity selection remains independent. - -Only the final application token is sent as `Authorization: Bearer ...`. No Soprano API ID/key, -managed-identity assertion, or incoming Entra ID token is forwarded. Missing settings, token-acquisition -failure, blank tokens, or tokens with 30 seconds or less remaining lifetime fail before provider HTTP; -there is no API-key fallback. Evaluation skips acquisition. A provider rejection is not retried. -Tokens are treated as opaque: the Function checks SDK expiry metadata, not custom JWT claims. -Soprano remains responsible for signature, issuer, audience, expiry, permissions, and account validation. - -With `EPP_ACCESS_TOKEN_CACHE_ENABLED=true` (the default), credential instances and their SDK caches -are reused for the configured tenant/application/identity. -JavaScript/Python use a [worker-local refresh loop](#credential-caching-and-refresh) for both -exchange stages. .NET warms at startup and fetches a replacement on a cache miss; it has no poller. -JavaScript bounds shared acquisition to 2.5 seconds independently of individual waiters; -.NET bounds each fetch to 2.5 seconds with cancellation linked to that caller. JavaScript links -cancellation through an SDK HTTP-client wrapper because `getToken` options alone are insufficient -in the installed SDK. Python bounds caller waits and SDK connect/read inactivity to 2.5 seconds; -shared synchronous retrieval may finish after a waiter leaves. It uses `get_token_info` for refresh -hints when supported, otherwise `get_token`; a failed acquisition never falls back to another API. - -Credential SDK transport retries are disabled. With caching enabled, JavaScript/Python failed refreshes use the polling -cadence below; .NET retries credential acquisition on a later cache miss. These are not end-to-end -delivery deadlines. JavaScript suppresses SDK logs in the -acquisition's asynchronous context. Python filters Azure Identity/Core/MSAL records on configured -handlers in that context; configure logging sinks before handling requests. .NET disables credential -diagnostics. Keep platform body tracing off and never log credential objects or tokens. - -Use the exact provider scope and complete send URLs selected by setup. Source changes do not -update deployed settings or establish provider authorization. Validate the current OAuth -configuration with your provider before activation. - -See [Microsoft's managed-identity federation guidance](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity). - -Confirm that the request's locale is supported by your Soprano endpoint and account. The Soprano -adapter uses `en-US` when the locale is absent or blank; a nonblank value is forwarded without -checking provider support. Test recipient audio separately from API acceptance. -This adapter uses the JSON `/messages/omnimsg` interface configured by the provider profile, -not a form-encoded voice API. Use the field names and response mapping in the selected adapter. - -JWE provides payload confidentiality and integrity, **not caller authentication**. Anyone with the -public key can encrypt a request. The nonce acknowledges decryption; it is not an authentication -credential or replay protection, and a fixed nonce cannot substitute for Easy Auth. - -### EPP response (JSON) + + +| Field | Description | +| --- | --- | +| `type` | **Required string.** Must be `microsoft.mfa.otpDeliver.v1`. | +| `channel` | **Required integer or string.** `1` or `"sms"` for SMS. `2` or `"voice"` for voice. Names are case-insensitive. | +| `mode` | **Required integer or string.** `1` or `"live"` for delivery. `2` or `"evaluation"` for no-send evaluation. Names are case-insensitive. | +| `encryptedDeliveryContext` | **Required string.** Compact JWE containing the delivery data. | +| `tenantId` | Optional string with routing metadata. It does not select authentication or a provider. | +| `correlationId` | Optional sign-in correlation string. | +| `ttlSeconds` | Optional positive JSON integer up to `2147483647`. | + +Use an integer such as `60` for `ttlSeconds`. Do not send `"60"`, `60.0`, `6e1`, `null`, a +boolean, or a nonpositive value. TTL validation does not enforce passcode expiry or a delivery +deadline. + +Unknown versions, unsupported channel or mode values, invalid TTL values, and missing encrypted +content return `400 Bad Request`. + +### Encrypted delivery context + + + +The compact JWE must: + +- Use `RSA-OAEP-256` for key wrapping. +- Use `A256GCM` for content encryption. +- Contain exactly five nonempty segments. +- Be no longer than 16,384 characters. +- Pass authentication-tag validation before the plaintext is used. + +Pass the original compact JWE to the JOSE library. Do not rebuild its protected header. +The protected `alg` and `enc` values are fixed. `RSA-OAEP`, `A128GCM`, and +`A256CBC-HS512` are rejected. + +The sample decrypts with the single RSA private key in `EPP_DECRYPTION_KEY_PEM`. In Azure, use a +Key Vault secret reference. `EPP_ENCRYPTION_KEY_ID` checks the JWE `kid` after decryption and logs +only `encryption_key_id_mismatch` on a mismatch. It does not select a key. + +See [certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle) for renewal +and key synchronization. + +The decrypted JSON contains: + +| Field | Description | +| --- | --- | +| `nonce` | **Required string.** Returned unchanged after evaluation or provider acceptance. | +| `phoneNumber` | **Required string.** Expected in E.164 form. | +| `message` | **Required string.** Localized message containing the passcode. | +| `locale` | Optional voice locale when supported by the provider. | +| `extension` | Optional office-voice data. The shared dispatcher does not forward it. | +| `riskContext` | Optional context data. The sample does not evaluate it. | +| `textToVoice` | Optional legacy speech data. | + +`nonce`, `phoneNumber`, and `message` must be nonblank strings. Decryption or completeness +failures return `400 Bad Request`. +The nonce confirms decryption in the response. It is not caller authentication or replay protection. + +Provider-specific message mapping belongs in the +[provider implementation guide](BRING-YOUR-OWN-PROVIDER.md#provider-request-and-response-mapping). + +## Modes + + + +### Evaluation + +Use `mode: 2` or `"evaluation"` to validate authentication, envelope parsing, and JWE decryption. +The Function returns the nonce without selecting a provider, reading provider credentials, or +sending a message. + +Easy Auth and the decryption-key reference still apply in Azure. Core Tools does not provide Easy +Auth, so local evaluation must stay on loopback without tunnels. + +### Live + +Use `mode: 1` or `"live"` to send through the configured provider. The Function returns success +only after the adapter recognizes provider acceptance. + +Provider acceptance is not proof of recipient delivery. Check delivery through the provider's +supported evidence. + +## Response + + + +### Success response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +``` ```json -{ "nonce": "", "correlationId": "", "providerStatus": "accepted" } +{ + "nonce": "", + "correlationId": "", + "providerStatus": "accepted" +} ``` -The endpoint returns **`200`** only after the adapter recognizes acceptance on a successful -provider HTTP response. A queued or pending status counts only when that adapter explicitly -recognizes it; a provider `2xx` alone is insufficient. Acceptance is not handset delivery. -`Evaluation` mode returns `200` + nonce echo without a provider send. - -The sample's integration contract expects the caller to treat `2xx` with a matching nonce as -handled; a nonce mismatch, non-`2xx` response, or timeout can lead to native fallback. -This repository does not implement or verify the Entra ID caller's current fallback, retry, -or timing behavior. Confirm it through the -[approved EPP procedure](ONBOARDING.md#validate-the-deployed-endpoint); -do not rely on fallback as a guaranteed recovery mechanism. - -Live handlers await provider acceptance; they do not launch background delivery after replying. -Handler failures omit the nonce and accepted status and return a sanitized error with a request ID -and, after envelope processing, a correlation ID. Platform rejections happen before the handler and -do not use this application response contract. - -Handler validation failures return `error: "bad_request"` and a fixed `reason`. JavaScript/Python -envelope checks run in this order: object shape, version, encrypted-context presence, channel, mode, TTL. -Their reasons are `invalid JSON body`, `invalid envelope`, `unsupported envelope type`, -`encryptedDeliveryContext is required`, `unsupported channel`, `unsupported mode`, `invalid ttlSeconds` -or `ttlSeconds expired`. A decrypted context missing a required nonblank string returns -`incomplete delivery context`. Reasons never include supplied values or exception text. JWE failures -return `error: "decryption_failed"` without a cryptographic reason or nonce. - -### Runtime binding differences - -Use the request forms above across runtimes, but expect differences in parsing and errors. -JavaScript/Python parse JSON inside the handler. The .NET Functions host first binds the JSON -to a typed `EntraSendOtpPayload`, then calls the handler. - -- .NET treats null `ttlSeconds` as omitted, and its enum converter accepts numeric - strings such as `"1"`. JavaScript/Python reject those forms. Send an integer TTL or omit it, - and use integer channel/mode codes or named strings for portability. -- Malformed JSON and incompatible property types can fail .NET host binding before the - handler's sanitized error response. For payloads reaching its validation, .NET uses - `unsupported payload type` rather than `unsupported envelope type`; a null bound payload - produces `invalid payload`. Do not assume a host-binding error has handler JSON fields. -- Required decrypted fields must be nonblank strings. .NET deserializes them as strings, so - incompatible types can produce `decryption_failed` before the completeness check. - JavaScript/Python instead check their raw values for completeness. Optional fields are not - uniformly modeled: only `locale` accompanies the required fields in .NET's delivery context; - JavaScript/Python also retain `extension` and `riskContext`, but shared dispatch omits both. -- For normal nonempty string IDs, the response uses body `correlationId` first, then - `x-ms-correlation-id`, then a generated `requestId`. JavaScript uses the raw header; - Python/.NET sanitize it. .NET preserves an empty body string; JavaScript/Python fall back. - Early validation errors omit correlation; later handled errors include it. - Python's unexpected-error path can return null correlation when a parsed payload lacks it. -- `x-ms-client-request-id` supplies the internal `messageId`, with a generated fallback. - JavaScript uses the raw header; Python/.NET sanitize it. Neither request ID nor correlation - ID provides deduplication. - -See the handlers for exact binding and response paths: +The response contains: + +| Field | Description | +| --- | --- | +| `nonce` | Original decrypted nonce. | +| `correlationId` | Body value, then `x-ms-correlation-id`, then a generated request ID. | +| `providerStatus` | Always `"accepted"` for evaluation or recognized live acceptance. | + +Failures omit `nonce` and `providerStatus`. Handler errors include a generated `requestId` and +may include a fixed `reason` and `correlationId`. Platform authentication and .NET host-binding +errors can occur before this JSON response is created. + +The caller uses a successful response with the matching nonce as the handled result. This +repository does not implement or guarantee caller fallback, retry, or timing behavior. Validate +those behaviors through the approved EPP procedure. + +Handler failures use `error: "bad_request"` or `error: "decryption_failed"`. Fixed validation +reasons include: + +- `invalid JSON body` +- `invalid envelope` +- `invalid payload` +- `unsupported envelope type` +- `unsupported payload type` +- `encryptedDeliveryContext is required` +- `unsupported channel` +- `unsupported mode` +- `invalid ttlSeconds` +- `ttlSeconds expired` +- `incomplete delivery context` + +## HTTP status codes + + + +| Status | Meaning | +| --- | --- | +| `200 OK` | Evaluation completed or the provider returned a recognized accepted status. | +| `400 Bad Request` | Invalid JSON, envelope, JWE, delivery context, or provider request. | +| `401 Unauthorized` | Easy Auth rejected the caller, or the provider rejected its credentials. | +| `403 Forbidden` | Easy Auth denied the caller, or the provider returned a recognized block. | +| `429 Too Many Requests` | The provider returned `429`. | +| `502 Bad Gateway` | Missing provider configuration, network failure, or unrecognized provider response. | +| `504 Gateway Timeout` | The provider request timed out. | + +Unknown, incomplete, or unsuccessful provider responses fail closed. A provider `2xx` response is +not enough unless the adapter recognizes its status as accepted. + +A timed-out live request might already have reached the provider. Check its outcome before retrying. + +## Runtime notes + + + +Use the request forms above for every runtime. + +- **Parsing:** JavaScript and Python parse JSON inside the handler. .NET binds to + `EntraSendOtpPayload` first, so malformed JSON or incompatible types can fail before the normal + error body is created. +- **Channel and mode:** .NET can bind numeric strings such as `"1"`. JavaScript and Python reject + them. Use integer codes or the documented names. +- **TTL:** JavaScript normalizes `60.0` and `6e1` to `60`. Python and .NET reject those forms. + .NET treats `null` as omitted. Send an integer or omit the field. +- **Validation:** .NET uses `unsupported payload type` where JavaScript and Python use + `unsupported envelope type`. A null .NET payload returns `invalid payload`. Incompatible + decrypted field types can return `decryption_failed` in .NET before its completeness check. +- **Delivery context:** .NET models `locale`. JavaScript and Python also retain `extension` and + `riskContext`. Shared dispatch does not forward the latter fields. +- **Correlation:** normal string values use the body, then `x-ms-correlation-id`, then a generated + ID. JavaScript uses the raw header. Python and .NET sanitize it. .NET preserves an empty body + value while JavaScript and Python fall back. Early validation errors omit correlation. +- **Request ID:** `x-ms-client-request-id` supplies `messageId`. JavaScript uses the raw header. + Python and .NET sanitize it. Request and correlation IDs do not provide deduplication. + +See the runtime handlers for exact binding behavior: [JavaScript](../javascript/src/functions/SendOtp.js), -[Python](../python/function_app.py), and [.NET](../dotnet/Functions/SendOtp.cs). - -### Evaluation (no provider send) - -The only shared non-delivery control is the existing incoming `mode: 2` or `mode: "evaluation"`, -for every provider and language. On Azure, Easy Auth still authenticates the caller before the -handler validates the envelope and decrypts the JWE with integrity checks. Provider lookup, provider -Key Vault reads and outbound provider HTTP are skipped. No provider name, endpoint or credentials -are needed. Platform authentication and resolution of the decryption-key reference may still require -network access. Core Tools has no Easy Auth; local evaluation must remain loopback-only, without tunnels. - -This describes the evaluation **request path**. Independently, workers with a configured provider -and its cache enabled prepare credentials at startup; JavaScript/Python also poll for refresh, -even if their current traffic is evaluation-only. -No background task dispatches an OTP. A worker without `EPP_PROVIDER_NAME` performs no credential -prewarming. Disabling the selected cache also suppresses this preparation; evaluation never requires it to succeed. - -There is no diagnostic environment flag. A live request is not an evaluation request. Adapter-specific -wire fields, where required by an API, remain internal and cannot enable a separate non-delivery mode. - ---- - -## 2. Outcome → HTTP status mapping - -The provider interprets its parsed status as an **outcome** and endpoint HTTP status. -**Fail-closed:** an unknown/unmapped or incomplete response is treated as `Fail`. -An unsuccessful provider HTTP response cannot become `Continue` because its body contains a -success-looking status. Explicit `Block` outcomes remain non-success responses. -The current runtime outcome enums contain only `Continue`, `Block`, and `Fail`; there is no -implemented `StepUp`/`409` mapping. - -| Outcome | HTTP | When | -|---------|------|------| -| `Continue` | `200` | recognized success status | -| `Block` | `403` | provider says blocked | -| `Fail` | `429` | provider returned 429 | -| `Fail` | `401` | provider returned 401/403 (auth) | -| `Fail` | `400` | other provider 4xx | -| `Fail` | `502` | other provider error, or missing credential/endpoint | -| N/A | `400` | configured provider name is missing or not registered | -| N/A | `504` | request to the provider timed out | -| N/A | `502` | network error to the provider (non-timeout) | - ---- - -## 3. Provider contract - -Each provider owns: - -- its fixed name, authentication mode and credential acquisition specification; -- request construction, including provider-specific paths, headers and JSON; -- response shape interpretation, recognized statuses, outcome and endpoint HTTP status; -- safe failure classification: `provider_http_error`, `provider_rejected`, - `unrecognized_provider_status`, `missing_provider_message_id` or `invalid_provider_json`. - -The shared transport owns final HTTPS validation, timeout handling, disabled redirects, and -response-body reading and decoding. JavaScript/.NET propagate cancellation through body reading; -Python uses connect/read inactivity timeouts. The provider receives -the transport result and returns a normalized provider result. Raw API-specific JSON is not inspected -by the Function and never enters public responses or logs. - -This model is internal: do not serialize it into the endpoint response or logs. The -[request logger](LOGGING.md) selects only the provider HTTP status, a recognized normalized -status, the mapped outcome and a bounded raw provider message ID; descriptions and raw -metadata remain private. Public HTTP responses still expose only the existing -nonce/correlation/status or sanitized error contract. -Provider requests are serialized when building the outbound HTTP body. JavaScript/Python map -parsed response objects; .NET deserializes provider-specific response types through -`PhoneProviderBase.SendJsonAsync`. The handler uses only the normalized result. - -Consult the selected provider implementation for required credentials and options. The -[onboarding credential naming table](ONBOARDING.md#provider-credential-names) lists exact secret -names for provisioning and authorized local tests. Keep that table aligned with provider credential -specifications; never include secret values in documentation or the settings sample. - -### Telesign EPP integration - -SMS and Voice use the complete provider-approved URLs selected from the provider profile. The provider -supplies `recipient.phone_number`, `message.text` (unchanged for SMS, paced/repeated for voice), optional `message.language`, one -selected `channels[].channel`, and `correlation_id`. Keep the leading `+` in the E.164 phone number. - -This sample implements Basic authentication only. Per -[Telesign's authentication instructions](https://developer.telesign.com/enterprise/docs/authentication#basic-authentication), -the header is `Authorization: Basic `, using the raw Customer ID -and API Key strings from Key Vault. Do not decode the API key first, send the API key alone, or -substitute a key identifier. The guide's `Basic YOUR_API_KEY` is abbreviated, not the literal encoding. -Digest and provider-token authentication are not implemented for Telesign here. - -The optional `account_lifecycle_event` and `originating_ip` fields are reserved for future intelligence -capabilities. They are omitted; do not infer an originating address from the Function or synthesize -account events to fill them. - -Telesign's `X-Shutter-Mode: true` suppresses delivery at the provider while still calling its endpoint. -It is appropriate for an explicitly authorized, direct provider diagnostic, not normal OTP delivery. -The production provider does not add or forward this header. Function evaluation mode remains separate: -it validates/decrypts and skips all provider HTTP. A successful provider shutter probe is not evidence -that an SMS was delivered or a Voice call was placed. Enabling the API globally also does not prove -that a particular Customer ID/API Key pair is authorized for this integration. - ---- - -## 4. Configuration (app settings / env) - -Set by provisioning. **Identical names across all languages.** - -| Key | Purpose | -|-----|---------| -| `EPP_PROVIDER_NAME` | fixed id of the selected provider; `` is a placeholder, not a bundled default | -| `EPP_PROVIDER_SMS_ENDPOINT` | required SMS endpoint for the selected provider/region; complete send URL for Telesign/Soprano, base URL for Infobip/Sinch | -| `EPP_PROVIDER_VOICE_ENDPOINT` | required voice endpoint for the selected provider/region; complete send URL for Telesign/Soprano, base URL for Infobip/Sinch; may differ from SMS | -| `EPP_PROVIDER_ENDPOINT_REGION` | selected `global` or `eu` route label; informational at runtime | -| `EPP_PROVIDER_AUTH_MODE` | must match the selected adapter (`apiKey` for Infobip, Sinch, or Telesign; `oauth` for Soprano); see [guided versus adapter-only support](ONBOARDING.md#provider-credential-names) | -| `EPP_PROVIDER_TENANT_ID` | selected provider tenant; added to the Step 1 app's allowed-tenants preview and used as the OAuth authority for Soprano | -| `EPP_PROVIDER_SCOPE` | Soprano 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 provider | -| `EPP_PROVIDER_TIMEOUT_MS` | trimmed ASCII decimal milliseconds; default 1500 for missing/invalid/nonpositive values; capped at 2500. Not a whole-invocation deadline | -| `EPP_KEY_VAULT_CACHE_ENABLED` | `true`/`false`: API-key bundle caching and startup/refresh; unset defaults to `true` | -| `EPP_ACCESS_TOKEN_CACHE_ENABLED` | `true`/`false`: OAuth credential caching and startup/refresh; unset defaults to `true` | -| `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 for API-key providers | -| `AZURE_CLIENT_ID` | set for a user-assigned managed identity | - -Telesign credentials live in **Key Vault**, under the names in its credential specification, and are fetched via -managed identity. Soprano exchanges an outbound managed-identity assertion for a token in the -configured provider tenant/scope. Do not put provider secrets in code or app settings. - -Caller trust is configured in **Easy Auth**, not application environment variables: pin the trusted -tenant issuer, the endpoint-app audience and the authorized Microsoft caller application ID. Incoming -`tenantId` is routing metadata and cannot select any of these. There is no application host-detection -guard or backup token validation. See [platform onboarding](ONBOARDING.md#2-provision-encryption-and-deployment-trust). - -### Default provider and configuration readers - -Provision `EPP_PROVIDER_NAME` with the customer's selected provider, plus -`EPP_PROVIDER_SMS_ENDPOINT`, `EPP_PROVIDER_VOICE_ENDPOINT`, and matching authentication settings. -The endpoint settings can use different URLs for the same provider. A missing or unknown -provider fails closed; there is no implicit default or automatic failover. Request-body provider -fields are not used. - -For each live request, the matching channel-specific endpoint is required. A missing, empty, or -invalid selected endpoint fails with `502`; there is no shared endpoint fallback, and one channel -never borrows the other channel's URL. The unselected channel's endpoint does not affect the request. -Endpoints must be absolute HTTPS URLs with a hostname, port 1-65535, and no userinfo or fragment; redirects are -not followed. Evaluation does not require provider endpoints. - -Telesign and Soprano send to the selected URL unchanged. Infobip and Sinch use the selected -channel-specific base URL and append their API paths. Neither adapter substitutes a default base URL. - -Each valid request's `channel` selects SMS or voice, without a deployment-level channel restriction. -The legacy `EPP_PROVIDER_CHANNEL` setting is not read. Unsupported request channels still fail -validation; provider account access for each intended channel remains an onboarding requirement. - -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). -They return named configuration objects for encryption and the selected provider, not caller-authentication -settings. Key Vault settings are read by JavaScript's configuration object and by the Python/.NET secret resolvers. -Provider-specific options remain ordinary app settings passed to the selected provider. - -JSON parsing and type checks stay at the request boundary. Downstream code uses `Envelope`, -`DeliveryContext` and provider-neutral delivery models (immutable JavaScript objects, dataclasses -in Python, and classes/records in .NET). Named .NET response records preserve the existing wire names -and optional-field omission. Object construction does not replace validation or coerce invalid input. - -All customers call the same `POST /api/SendOtp` handler in their chosen language. A fixed lookup selects -the configured provider, which builds the SMS or voice API call. Purchasing an unsupported -provider does not install an implementation: add that provider first. Purchase, -subscription activation and changing tenant policy belong to provisioning, not this Function. - -### Credential caching and refresh - -Provider credentials are process-local, distinct from the platform-resolved decryption-key -reference. Credential acquisition never sends an OTP or changes caller authentication. -All runtimes use `EPP_KEY_VAULT_CACHE_ENABLED` for the selected `apiKey` provider or -`EPP_ACCESS_TOKEN_CACHE_ENABLED` for the selected `oauth` provider. The switches are independent; -runtime selection does not depend on the hosting plan. Unset defaults to enabled. Values accept -trimmed, case-insensitive `true` or `false`; blank or other explicit selected values fail live -credential acquisition closed with a sanitized warning, without blocking evaluation. -Restart workers after configuration changes. Setup writes both as `false` for FC1 or `true` for EP1; -deploy a supporting package, since older releases do not read these settings. - -With caching **disabled**, each live request retrieves a complete Key Vault bundle or uses fresh -managed-identity/client-assertion SDK credentials for OAuth. There is no startup preparation, -periodic polling, cross-request credential sharing, or failure cooldown. Request-scoped state is -discarded after acquisition; Python closes its SDK clients when synchronous acquisition finishes. -The same expiry checks and acquisition budgets below still apply. Azure's managed-identity service -and platform Key Vault-reference caching remain outside these switches. - -With caching **enabled**, each runtime retains its existing policy: - -| Runtime | Startup and replacement behavior | Operator consequence | -|---|---|---| -| [JavaScript](../javascript/src/functions/credentials.js) | Selects `ApiKeyCache` (`lru-cache`) or `AccessTokenCache`; warms at startup and polls every 30 seconds. API-key bundles target refresh at four minutes and expire at five. OAuth reuses SDK credentials/caches and the latest usable token. | Warm reads do not wait for refresh. Concurrent cold callers share acquisition; failures are retried at the polling cadence, not by sending another OTP. | -| [Python](../python/src/credentials.py) | Equivalent selected-cache/polling policy using `cachetools.TTLCache` and a daemon initialization/refresh thread. | Callers can stop waiting while synchronous acquisition remains shared. Connect/read inactivity timeouts are not a hard total I/O deadline. | -| [.NET](../dotnet/Src/CredentialTokenService.cs) | Hosted service warms the selected provider once. `MemoryCache` holds provider-supplied credentials until absolute expiry; the next miss fetches again. No periodic refresh loop. API-key credentials expire after five minutes; Soprano entries expire 30 seconds before SDK token expiry. | The first request after expiry can pay acquisition latency. `GetOrCreateAsync` does not guarantee single-flight retrieval for concurrent misses. A quiet app need not emit periodic credential events. | - -JavaScript/Python retry failed refresh on a later poll without extending cached credential -lifetime; requests with expired credentials join acquisition or fail closed. SDK token expiry -is preserved, and tokens with 30 seconds or less remaining are unusable. These implementations -stop polling/prevent late publication at shutdown. .NET has no poller to stop: failed cache -fills are not retained and later misses attempt retrieval. It does not proactively refresh a -still-valid entry. - -JavaScript limits shared acquisition to 2.5 seconds and passes cancellation through the SDK -transport. Python limits waits and SDK connect/read inactivity to 2.5 seconds. Synchronous I/O -may continue until the underlying call returns. .NET uses a 2.5-second fetch budget linked to -the caller's cancellation token. None is a whole-invocation deadline. - -All runtimes fetch a complete API-key/customer-ID bundle before use and use managed identity -for vault access. With caching enabled, Soprano reuses managed-identity and client-assertion SDK credentials -without Key Vault or a client-secret fallback. Evaluation skips credential resolution on the -request path even when independent startup/refresh work runs. - -**Prewarming is not traffic readiness or a latency guarantee.** Test cold, warm, expired, rotated, -and concurrent cases for the deployed runtime. The `credential_refresh_failed` event name is also -used by .NET for startup/cache-fill failures; it does not imply a periodic job. Field casing and -collection differ by runtime; use [runtime-specific discovery](MONITORING.md#runtime-specific-log-discovery). -Do not log SDK exception details or tokens. No cache retries/deduplicates provider sends. - ---- - -## 5. Required behaviors - -- **Safe outcome mapping (fail-closed)**: only `Continue` → `200 accepted`; unknown status → `Fail`. -- **Managed identity**: Key Vault access via managed identity only (user-assigned if `AZURE_CLIENT_ID` - set, else system-assigned). No static credentials. -- **Privacy**: never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads, - raw exceptions, provider descriptions/responses or endpoint query strings. There is no plaintext diagnostic - override. Each handler emits separate service events. JavaScript and .NET use fixed completion - events rather than a mutable comprehensive summary; Python uses the same fixed-event approach - through the standard `logging` pipeline. - Generated Function IDs remain distinguished from raw Microsoft/provider support IDs. Original wire IDs - and the required nonce echo remain unchanged. Support IDs can correlate customer activity; restrict - log access and retention. Endpoint logs contain only scheme, host/port and API path, never userinfo, - query strings or fragments. A configured encryption-key-ID mismatch adds a correlated fixed warning, - never either key ID or the JWE header. Disable SDK, platform and proxy body tracing separately. -- **Platform authentication only**: enable Easy Auth with `requireAuthentication=true`, - `unauthenticatedClientAction=Return401` and `requireHttps=true`. Configure the trusted tenant issuer - and `allowedAudiences` for the endpoint app, plus a **nonempty `allowedApplications`** list pinned to - the authorized Microsoft caller application ID. No excluded path may bypass authentication for SendOtp. - The trigger remains `authLevel: anonymous`; there is no application token validation or function-key - fallback. The platform rejects unauthenticated requests with `401` and denies callers outside the - allowlist before the handler. **Keep Easy Auth enabled for every public SendOtp route.** - Core Tools supplies no Easy Auth, so local execution stays on loopback without tunnels or public - forwarding. Request data, JWE decryption, a fixed nonce, and forwarded principal headers serve - other parts of the contract; Easy Auth authenticates the Entra ID caller. -- **Timeout boundaries**: platform authentication and Key Vault retrieval happen outside the outbound HTTP - timer. Python uses connect/read inactivity timeouts, not a hard elapsed-time deadline. The cap - does not guarantee a 3.2-second end-to-end response, especially on cold starts. - A timed-out POST may already have been accepted; avoid blind retries that duplicate messages. - -### Application logs - -See the [application log reference](LOGGING.md) for event order, allowlisted fields, identifier -provenance, levels, and runtime formats. Each invocation emits fixed events ending in -`request_completed`, not a mutable request summary. Evaluation emits no provider events. -Keep bodies, nonces, credentials, and raw exceptions out of every pipeline. - -Emission is not guaranteed collection. Use [runtime-specific queries](MONITORING.md#runtime-specific-log-discovery) -and [collection diagnostics](APPLICATION-INSIGHTS.md) before relying on alerts. - ---- - -## 6. Lightweight tests - -Each language keeps lightweight offline tests covering representative application checks for: - -- Bundled provider request formats and static provider credentials. -- Fail-closed outcomes, missing credentials, HTTPS guards and timeouts. -- Envelope validation and real JWE decryption/tamper rejection. -- Evaluation handling without provider I/O; configured-provider startup refresh is tested separately. -- Awaited delivery, nonce acknowledgement and privacy-safe logging, including the shared - service-event order and safe field cases in [contract.json](../tests/fixtures/contract.json), - identifier provenance, error paths, provider-body timeouts and concurrent request isolation. -- Runtime-specific credential caching using fake dependencies: JavaScript/Python cover selected-cache - startup, shared retrieval, polling, expiry, and shutdown; .NET covers its startup/cache-miss model. - JavaScript tests also exercise cancellation through the actual SDK pipeline. - -The sample deliberately omits exhaustive input permutations and SDK internals. These tests use -local keys and mocked external services; they do not send SMS and **do not test Easy Auth or platform -authorization**. Separate deployed security tests are required for missing/invalid credentials, -wrong issuer or audience, unauthorized caller, HTTPS enforcement and SendOtp route protection. -An authorized evaluation must succeed without provider I/O. These checks do not replace provider -integration or handset-delivery checks. See [deployment verification](ONBOARDING.md#4-package-deploy-and-verify). +[Python](../python/function_app.py), and +[.NET](../dotnet/Functions/SendOtp.cs). + +## Provider implementation + + + + + + + + +The HTTP contract is provider-neutral. Use +[Bring your own provider](BRING-YOUR-OWN-PROVIDER.md) for: + +- Provider request and response mapping. +- Settings and channel endpoints. +- API-key and OAuth authentication. +- Credential caching and refresh. +- Soprano and Telesign behavior. +- Packaging, tests, and production planning. + +App settings keep the `EPP_` prefix. Shared code uses the `Envelope` and `DeliveryContext` models. + +## Required behavior + + + -## Production readiness - -Use this checklist to plan capabilities required beyond the sample. -Provider acceptance and nonce checks confirm important stages of the request, while handset -delivery and provider-specific features need their own validation. - -- **Encryption and rotation:** the Function imports one private PEM through a Key Vault secret - reference and decrypts in-process. A production design can add vault-resident cryptographic - operations and overlapping key rotation when required. -- **Delivery workflow:** the sample waits for provider acceptance before replying. Add a durable - queue, early acknowledgement/background handoff, or automated retry strategy only when the - approved product design defines their timing and duplicate-send behavior. -- **End-to-end timing:** size the caller budget for cold starts, platform authentication, Key Vault - access, and the provider request. The outbound timeout covers the provider call rather than the - full invocation; Python uses connect/read inactivity timeouts. -- **Credential readiness:** caches are process-local and proactive preparation is best-effort. - Provider delivery still waits for acceptance; background credential refresh is separate from - OTP delivery, queuing, and duplicate-send protection. -- **Voice experience:** Soprano extracts the first six consecutive digits; Telesign paces - standalone six-digit numeric runs and repeats the full message. Validate supported locales, - passcode formats, and actual audio for the intended experience. -- **Additional production capabilities:** add the body-size/content-type and E.164 validation, - subscription setup, certification, least-cost routing, or voice-callback workflows required - by the design. Native caller fallback remains with the caller rather than this Function. - -See [setup compatibility](ONBOARDING.md#setup-script-compatibility) for credential provisioning, -endpoint format and unsupported setup options. + + +- Keep Easy Auth enabled for every public `/api/SendOtp` route. +- Use managed identity for Key Vault access. +- Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, raw exceptions, + and provider responses out of logs. +- Do not forward the incoming bearer token to a provider. +- Do not retry an uncertain live send without checking the provider outcome. +- Test evaluation, live acceptance, recipient delivery, caller rejection, invalid payloads, and + tampered JWE independently. + +See: + +- [Application log reference](LOGGING.md) +- [Provider tests and production readiness](BRING-YOUR-OWN-PROVIDER.md#before-activation) +- [Deployment validation](ONBOARDING.md#validate-the-deployed-endpoint) diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index bd4023a..019a1d7 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -44,7 +44,7 @@ Prepare: Check the [full prerequisites](../setup/docs/README.md#prerequisites-for-step-2), including the Microsoft Entra ID allowed-tenants preview. Setup can install Bicep, but not Azure CLI. -Assign an **onboarding owner** and review [production readiness](CONTRACT.md#production-limitations). +Assign an **onboarding owner** and review [production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness). ## 2. Register your application diff --git a/docs/MONITORING.md b/docs/MONITORING.md index cebb653..e030ba2 100644 --- a/docs/MONITORING.md +++ b/docs/MONITORING.md @@ -392,7 +392,7 @@ See [event order and identifier provenance](LOGGING.md). `credential_refresh_failed` also differs operationally: JavaScript/Python have a polling refresh loop, while .NET emits this event on failed startup/cache-fill acquisition and has no -periodic poller. See [credential behavior](CONTRACT.md#credential-caching-and-refresh). +periodic poller. See [credential caching](BRING-YOUR-OWN-PROVIDER.md#credential-caching). Absence of a warning, especially in an idle app or a missing export pipeline, is not a credential health check. diff --git a/docs/MULTI-PROVIDER-IMPLEMENTATION.md b/docs/MULTI-PROVIDER-IMPLEMENTATION.md index 34fa0f2..9f5faa2 100644 --- a/docs/MULTI-PROVIDER-IMPLEMENTATION.md +++ b/docs/MULTI-PROVIDER-IMPLEMENTATION.md @@ -153,11 +153,11 @@ module.exports = { stopProviderCredentialRefresh }; Remove the singleton import only after replacing its request/lifecycle uses. With its selected cache enabled, each service begins periodic refresh when first used. With caching disabled, each live attempt acquires credentials without a poller or cross-request reuse. Preserve -the [cache switches](CONTRACT.md#credential-caching-and-refresh) in each account's configuration; -setup sets them to `false` for FC1 and `true` for EP1, while omitted values default to `true`. -An evaluation request must not start credential acquisition; -already-running refresh is independent. Rebuild contexts on controlled restart when configuration -changes; never repurpose a live service for another account. +the [cache switches](BRING-YOUR-OWN-PROVIDER.md#credential-caching) in each account's configuration. +Setup sets them to `false` for FC1 and `true` for EP1. Omitted values default to `true`. +An evaluation request must not start credential acquisition. Already-running refresh is independent. +Rebuild contexts on controlled restart when configuration changes. Never repurpose a live service +for another account. ## 4. Operation state diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 32e6c3e..8a6959b 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -28,7 +28,7 @@ missing access. Azure subscription ownership does not grant EPP access. Assign an **onboarding owner** to coordinate tenant policy, Azure, the provider, and Microsoft support. -Use the [production-readiness considerations](CONTRACT.md#production-limitations) to plan the +Use [production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness) to plan the capabilities your live service needs, such as durable delivery, retry and deduplication strategy, an end-to-end deadline, certificate renewal, and delivery confirmation. @@ -146,7 +146,8 @@ setup-created encryption certificate unchanged. grant broad access, or copy credentials into app settings to bypass a denial. 3. Complete the [authorized evaluation and live checks](#validate-the-deployed-endpoint). Evaluation does not validate these secrets. Setup disables provider caching for FC1 and enables - it for EP1; plan first-use/rotation checks from the [cache contract](CONTRACT.md#credential-caching-and-refresh). + it for EP1. Plan first-use and rotation checks from + [credential caching](BRING-YOUR-OWN-PROVIDER.md#credential-caching). No warning is not proof of account authorization. Use portal secret entry rather than command-line literal values, transcripts, source files, diff --git a/docs/README.md b/docs/README.md index d6283b8..c6f267f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,7 @@ approved test and activation process before deploying in a nonproduction environ - [Architecture and implementation](../TECHNICAL.md) - [HTTP contract](CONTRACT.md) - [Log schema](LOGGING.md) -- [Production readiness](CONTRACT.md#production-limitations) +- [Production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness) ## Deploy and validate diff --git a/dotnet/README.md b/dotnet/README.md index f5ddd51..991b319 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -75,8 +75,8 @@ Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-sen authentication and decryption without a provider send. Live success confirms acceptance, **not handset receipt**. Check a timeout before retrying. -For message/voice mapping, see the [Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe) -and [Telesign integration](../TECHNICAL.md#telesign-epp). +For message/voice mapping, see the [Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) +and [Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). ## Source @@ -99,7 +99,7 @@ accounts require a separate trust, configuration, credential, and test design. switches default to `true`; disabling the selected switch skips warmup and fetches on every live request. Enabled credentials remain in `MemoryCache` until absolute expiry. There is no polling timer, and concurrent misses can fetch more than once. The fetch has a 2.5-second cancellation -budget. See the [shared cache contract](../docs/CONTRACT.md#credential-caching-and-refresh). +budget. See [credential caching](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). | Source | Purpose | |---|---| @@ -122,4 +122,4 @@ method delegates to the base `SendJsonAsync` transport and maps the t `ProviderResult`; provider mapping code never handles `HttpResponseMessage` or raw JSON DOM types. Each provider keeps its own response rules. Common async HTTP transport lives in `PhoneProviderBase`. See -[production-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service. +[production readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. diff --git a/javascript/README.md b/javascript/README.md index d903412..64e10a8 100644 --- a/javascript/README.md +++ b/javascript/README.md @@ -70,8 +70,8 @@ authentication and decryption without a provider send. Live success confirms acc **not handset receipt**. Check a timeout before retrying. For message/voice mapping and credential rules, see the -[Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe) and -[Telesign integration](../TECHNICAL.md#telesign-epp). +[Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) and +[Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). ## Source and extension points @@ -83,13 +83,13 @@ that meets their requirements. For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and the [JavaScript event queries](../docs/MONITORING.md#4-javascript-service-event-queries). -The [cache switches and defaults](../docs/CONTRACT.md#credential-caching-and-refresh) apply here. +The [cache switches and defaults](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) apply here. Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. With caching enabled, API keys use Key Vault and `lru-cache`; OAuth uses the managed-identity/Entra SDKs. A 30-second refresh loop and one in-flight acquisition support warm reads. See the -[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh). +[credential caching guide](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). | Source | Purpose | |---|---| @@ -110,4 +110,4 @@ To add a provider, implement its fixed `name`, `authenticationMode`, `credential [providers/index.js](src/functions/providers/index.js). `interpretResponse` returns a `ProviderResult` containing the provider-owned outcome, endpoint HTTP status and one safe fixed failure classification. Raw API JSON stays inside the provider. See -[production-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service. +[production readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. diff --git a/python/README.md b/python/README.md index 7e4b0b9..179e4a0 100644 --- a/python/README.md +++ b/python/README.md @@ -69,8 +69,8 @@ Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-sen authentication and decryption without a provider send. Live success confirms acceptance, **not handset receipt**. Check a timeout before retrying. -For message/voice mapping, see the [Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe) -and [Telesign integration](../TECHNICAL.md#telesign-epp). +For message/voice mapping, see the [Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) +and [Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). ## Source @@ -83,13 +83,13 @@ For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and [runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery). Verify which Python logging properties the host/export pipeline preserves. -The [cache switches and defaults](../docs/CONTRACT.md#credential-caching-and-refresh) apply here. +The [cache switches and defaults](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) apply here. Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. With caching enabled, API keys use Key Vault and `cachetools.TTLCache`; OAuth uses the managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the -[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh). +[credential caching guide](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). | Source | Purpose | |---|---| @@ -106,4 +106,4 @@ managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the 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-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service. +See [production readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md index 6774973..9b04c32 100644 --- a/setup/docs/Troubleshooting.md +++ b/setup/docs/Troubleshooting.md @@ -141,7 +141,7 @@ It does not verify publication, startup, Easy Auth, or delivery. **Check:** serving-app `EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED`. Setup writes both as `"false"` for FC1 or `"true"` for EP1 and restores them on rerun. **Fix:** deploy a supporting release and restart after edits; older packages ignore the switches. -The [cache contract](../../docs/CONTRACT.md#credential-caching-and-refresh) defines accepted values, +The [credential caching guide](../../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) defines accepted values, fail-closed invalid settings, and runtime differences. Disabling a cache does not eliminate live credential acquisition or affect the platform's decryption-key reference. From 3755346e7b037af89c5bab1f49dea48a340c1a57 Mon Sep 17 00:00:00 2001 From: Hou Chi Chan Date: Thu, 15 Oct 2026 15:56:06 -0700 Subject: [PATCH 2/5] docs: separate Twilio and built-in provider guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe53050-0d2b-4d05-bcc1-20f5b611b665 --- README.md | 57 ++-- TECHNICAL.md | 204 +++++++++++--- docs/BRING-YOUR-OWN-PROVIDER.md | 389 ++++++-------------------- docs/CONTRACT.md | 260 +++++------------ docs/GETTING-STARTED.md | 2 +- docs/MONITORING.md | 2 +- docs/MULTI-PROVIDER-IMPLEMENTATION.md | 4 +- docs/ONBOARDING.md | 4 +- docs/README.md | 60 ++-- dotnet/README.md | 17 +- javascript/README.md | 17 +- python/README.md | 16 +- setup/docs/Troubleshooting.md | 2 +- 13 files changed, 412 insertions(+), 622 deletions(-) diff --git a/README.md b/README.md index b913c14..2db7b88 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ Connect Microsoft Entra ID to a phone provider for SMS and voice one-time passwords. Guided setup deploys **one regional endpoint** using JavaScript, C#, or Python. -Start in a separate nonproduction environment and review [requirements](#requirements). +> [!IMPORTANT] +> Start in a separate nonproduction environment. Confirm provider access and Microsoft's approved +> test and activation process before deployment. ## Table of Contents @@ -12,10 +14,7 @@ Start in a separate nonproduction environment and review [requirements](#require - [Architecture](#architecture) - [Get started](#get-started) - [Guidance](#guidance) - - [Deploy and configure](#deploy-and-configure) - - [Operate](#operate) - - [Develop](#develop) - - [Advanced topics](#advanced-topics) +- [Advanced topics](#advanced-topics) - [Getting help](#getting-help) ## Features @@ -23,22 +22,22 @@ Start in a separate nonproduction environment and review [requirements](#require - JavaScript, C#, and Python implementations of one [HTTP and trust contract](docs/CONTRACT.md). - Guided Azure deployment with Easy Auth, Key Vault, and monitoring. - No-send evaluation for authentication and decryption checks. -- Provider-neutral [phone-service integration](docs/BRING-YOUR-OWN-PROVIDER.md). +- Built-in provider integrations and an [Add Twilio](docs/BRING-YOUR-OWN-PROVIDER.md) guide. ## Requirements -- Confirm [provider access and Microsoft's approved test and activation process](docs/GETTING-STARTED.md#1-get-ready). -- Prepare a nonproduction tenant, subscription, app registration, and unique resource prefix. +- Use a separate test environment with a Microsoft Entra tenant, Azure subscription, app + registration, and unique resource name prefix. - Check [roles, permissions, tools, regional availability, and quota](setup/docs/README.md#prerequisites-for-step-2). -- Plan for Azure, telemetry, and provider charges; the [cost estimate](docs/COST-CALCULATOR.md) is not a bill. -- Keep Easy Auth enabled for the approved Microsoft caller. +- 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. ## Architecture ![Microsoft Entra ID sends an encrypted request through Easy Auth to a Function, which uses Key Vault and calls the phone provider](docs/images/single-region-architecture.png) *Easy Auth checks the Microsoft caller before the Function decrypts the request. -Live success confirms provider acceptance; confirm delivery separately. Evaluation sends nothing. +Live success confirms provider acceptance. Confirm delivery separately. Evaluation sends nothing. See [architecture details](TECHNICAL.md#single-region-architecture). [Open the full-size diagram](docs/images/single-region-architecture.png).* @@ -52,33 +51,23 @@ Follow the **[five-step setup guide](docs/GETTING-STARTED.md)** in order: 4. [Connect and test](docs/GETTING-STARTED.md#4-connect-your-provider-and-test): configure provider access and run approved checks. 5. [Activate and hand over](docs/GETTING-STARTED.md#5-activate-and-hand-over): activate after validation and assign owners. -Before live use, review [production readiness](docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness). +Before live use, review [production readiness](TECHNICAL.md#production-readiness). + + + ## Guidance -### Deploy and configure - -- [Five-step setup](docs/GETTING-STARTED.md) -- [Configuration and validation](docs/ONBOARDING.md) -- [Setup reference](setup/docs/README.md) -- [Troubleshooting](setup/docs/Troubleshooting.md) - -### Operate - -- [Function monitoring and alerts](docs/MONITORING.md) -- [Application Insights collection](docs/APPLICATION-INSIGHTS.md) -- [Microsoft Entra ID monitoring](docs/SIGN-IN-MONITORING.md) -- [Certificate renewal and rollback](setup/docs/README.md#encryption-certificate-lifecycle) -- [Provider cost estimate](docs/COST-CALCULATOR.md) - -### Develop - -- Runtime guides: [JavaScript](javascript/README.md), [C#](dotnet/README.md), [Python](python/README.md) -- [Architecture and settings](TECHNICAL.md) -- [HTTP contract](docs/CONTRACT.md) -- [Log schema](docs/LOGGING.md) -- [Phone-service integration](docs/BRING-YOUR-OWN-PROVIDER.md) +| Goal | Start here | +| --- | --- | +| Deploy and configure | [Five-step setup](docs/GETTING-STARTED.md) | +| Find settings and provider details | [Technical reference](TECHNICAL.md) | +| Resolve a problem | [Troubleshooting](setup/docs/Troubleshooting.md) | +| Monitor the endpoint | [Monitoring](docs/MONITORING.md) | +| Review the API | [Send OTP](docs/CONTRACT.md) | +| Add Twilio | [Twilio integration guide](docs/BRING-YOUR-OWN-PROVIDER.md) | +| Work on a runtime | [JavaScript](javascript/README.md), [C#](dotnet/README.md), or [Python](python/README.md) | ### Advanced topics diff --git a/TECHNICAL.md b/TECHNICAL.md index 3b20c1c..a36e28e 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -7,7 +7,8 @@ This reference covers implementation details, app settings, and packages. Sections: [implementations](#implementations), [guided setup](#guided-epp-setup), [download](#download-a-function-zip), [build](#build-zips-locally), -[settings](#configure-environment-variables), [provider details](#provider-specific-integration-details), +[HTTP details](#http-implementation-details), [settings](#configure-environment-variables), +[built-in providers](#built-in-providers), [production readiness](#production-readiness), or [security](#security). For regional continuity, see [resiliency and business continuity](#resiliency-and-business-continuity). @@ -19,8 +20,8 @@ Microsoft Entra ID sends an authenticated, encrypted request. Easy Auth checks t Function decrypts the request, and the selected adapter submits a live message. Success means provider acceptance, not confirmed delivery. -East US is illustrative. API-key integrations use provider secrets; OAuth integrations use -managed-identity token exchange. See [credential caching](docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) +East US is illustrative. API-key integrations use provider secrets. OAuth integrations use +managed-identity token exchange. See [provider settings and caching](#provider-settings-and-credential-caching) and [runtime logs](docs/MONITORING.md#runtime-specific-log-discovery). An authorized **evaluation request (`mode: 2`)** returns the nonce without sending a message. @@ -40,10 +41,10 @@ It covers capacity, shared keys, dependencies, readiness, failover drills, and r | Python (v2 model) | Available | [python/](python/) | Choose **one language and one provider per Function App**. All implementations share the -[HTTP, configuration, and trust contract](docs/CONTRACT.md); runtime behavior can differ. +[HTTP, configuration, and trust contract](docs/CONTRACT.md). Runtime behavior can differ. -For local work, use the selected runtime guide. For a custom integration, start with -[Bring your own provider](docs/BRING-YOUR-OWN-PROVIDER.md). +For local work, use the selected runtime guide. To add Twilio, use +[Add Twilio](docs/BRING-YOUR-OWN-PROVIDER.md). ## Guided EPP setup @@ -54,7 +55,7 @@ Review [application permissions](setup/docs/README.md#step-1---manually-create-t and [certificate selection](setup/docs/README.md#encryption-certificate-selection). Setup pins one PEM secret version and downloads only the public certificate. -One deployment accepts SMS and voice; each request's `channel` selects its configured provider URL. +One deployment accepts SMS and voice. Each request's `channel` selects its configured provider URL. ## Download a Function ZIP @@ -65,7 +66,7 @@ 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/publish before deployment | +| .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 | Verify `SHA256SUMS.txt`, configure the runtime, settings, Key Vault, and **Easy Auth**, then build @@ -81,8 +82,8 @@ Run one packaging script from the repository root. It creates a local ZIP withou | 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` | +| .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` | ```powershell pwsh -File ./package-javascript.ps1 @@ -113,7 +114,8 @@ pwsh -File ./package-javascript.ps1 -OutputPath ./artifacts/epp-javascript-v2.zi `-OutputPath` works for all three scripts. Configure the destination and follow [deployment and validation](docs/ONBOARDING.md#4-package-deploy-and-verify). Never include secrets. -## The design in one line + +## Request flow 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. @@ -125,6 +127,69 @@ Request `tenantId`, `channel`, `mode` and `ttlSeconds` are request data, not ext 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. +## HTTP implementation details + +Use [Send OTP](docs/CONTRACT.md) for the public request and response. This section covers runtime +implementation details. + +### JWE validation and key handling + +The compact JWE must use `RSA-OAEP-256` and `A256GCM`, contain five nonempty segments, and stay +within 16,384 characters. The authentication tag is validated before plaintext is used. +`RSA-OAEP`, `A128GCM`, and `A256CBC-HS512` are rejected. + +Pass the original compact JWE to the JOSE library. Do not rebuild its protected header. + +`EPP_DECRYPTION_KEY_PEM` accepts a private-key PEM or a PEM certificate bundle containing the +private key, as text or base64. In Azure, use a versioned Key Vault secret reference. +`EPP_ENCRYPTION_KEY_ID` checks `kid` after decryption and logs +`encryption_key_id_mismatch` on mismatch. It does not select a key. + +### Request validation and runtime differences + +Required decrypted fields are nonblank `nonce`, `phoneNumber`, and `message` strings. +Validation reasons include: + +- `invalid JSON body` +- `invalid envelope` or `invalid payload` +- `unsupported envelope type` or `unsupported payload type` +- `encryptedDeliveryContext is required` +- `unsupported channel` +- `unsupported mode` +- `invalid ttlSeconds` +- `ttlSeconds expired` +- `incomplete delivery context` +- `decryption_failed` + +Runtime differences: + +- JavaScript and Python parse JSON inside the handler. .NET binds to `EntraSendOtpPayload` first. +- .NET accepts numeric strings for channel or mode. JavaScript and Python reject them. +- JavaScript normalizes `60.0` and `6e1` to `60`. Python and .NET reject those TTL forms. +- .NET treats a null TTL as omitted. +- JavaScript uses raw correlation and request-ID headers. Python and .NET sanitize them. +- Request and correlation IDs support tracing. They do not provide deduplication. + +### Provider result mapping + +The shared outcomes are `Continue`, `Block`, and `Fail`. + +| Result | HTTP status | +| --- | --- | +| Recognized acceptance | `200` | +| Recognized block | `403` | +| Provider `429` | `429` | +| Provider `401` or `403` | `401` | +| Other provider `4xx` | `400` | +| Missing configuration, network failure, or unknown response | `502` | +| Provider timeout | `504` | + +Unknown or incomplete provider responses fail closed. Check a timed-out live request before +retrying because the provider might already have accepted it. + +Provider adapters use these safe failure labels: `provider_http_error`, `provider_rejected`, +`unrecognized_provider_status`, `missing_provider_message_id`, and `invalid_provider_json`. + ## Configure environment variables For guided deployment, inspect the [values and ownership table](docs/ONBOARDING.md#values-and-ownership). @@ -142,12 +207,34 @@ For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript, | 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. | +| `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. | -| `EPP_ENCRYPTION_KEY_ID` | Optional | Expected encryption key ID; mismatch only produces an advisory warning. | +| `EPP_ENCRYPTION_KEY_ID` | Optional | Expected encryption key ID. A mismatch only produces an advisory warning. | + +### Provider settings and credential caching + +- **Provider:** `EPP_PROVIDER_NAME`, `EPP_PROVIDER_AUTH_MODE`, and + `EPP_PROVIDER_ENDPOINT_REGION`. +- **Endpoints:** `EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT`. +- **Timeout and account:** `EPP_PROVIDER_TIMEOUT_MS` and `EPP_PROVIDER_ACCOUNT_NAME`. +- **Key Vault:** `KEY_VAULT_URL` and optional `AZURE_CLIENT_ID`. +- **OAuth:** `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE`, `EPP_OUTBOUND_CLIENT_ID`, and + `EPP_OUTBOUND_MI_CLIENT_ID`. +- **Caching:** `EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED`. + +All runtimes use `ManagedIdentityCredential` for Key Vault and outbound token access. They do not +fall back to a developer's Azure CLI identity. + +The provider timeout defaults to `1500` and is capped at `2500` milliseconds. -See [provider settings](docs/BRING-YOUR-OWN-PROVIDER.md#provider-settings) for provider IDs, -endpoints, authentication, credentials, and caching. +Each live request uses the endpoint for its selected channel. Endpoints must be absolute HTTPS +URLs with a hostname, valid port, no user information, and no fragment. Redirects are not followed. +There is no shared endpoint fallback or automatic provider failover. + +Cache switches accept `true` or `false` and default to `true`. JavaScript and Python warm at startup +and poll every 30 seconds. .NET warms once and refreshes on a cache miss. Disabling the selected +cache obtains credentials for each live request. Other values fail live credential acquisition. +Evaluation skips provider credential work. Credential acquisition has a 2.5-second budget. 1. **Locally:** create private local settings beside the chosen runtime's host file, following its [JavaScript](javascript/README.md#environment-configuration), [Python](python/README.md#environment-configuration) @@ -158,14 +245,11 @@ endpoints, authentication, credentials, and caching. Evaluation needs the decryption key but not provider settings. Use offline tests or loopback-only evaluation locally. Never commit local settings, keys, or test credentials. -For cache behavior, see -[credential caching](docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). - 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.azure.net/secrets//)` +or base64 PEM directly. Use a reference such as `@Microsoft.KeyVault(SecretUri=https://.vault.azure.net/secrets//)` for `EPP_DECRYPTION_KEY_PEM` in Azure app settings, where the platform resolves it. 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 +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 @@ -173,28 +257,84 @@ the Function or update Entra. See [certificate lifecycle](setup/docs/README.md#e 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 -configured provider or authentication. `channel` selects the configured SMS or voice endpoint; +configured provider or authentication. `channel` selects the configured SMS or voice endpoint. there is no deployment-level channel restriction. The legacy `EPP_PROVIDER_CHANNEL` setting is ignored. -## Provider-specific integration details + +## Built-in providers + + +Guided setup supports the sample's built-in **Soprano** and **Telesign** integrations. Use the [credential and deployment-support table](docs/ONBOARDING.md#provider-credential-names) -for onboarding. Implementation details are in: +for onboarding. + +### Soprano + +Soprano uses OAuth client-assertion exchange with: + +- `EPP_PROVIDER_TENANT_ID` +- `EPP_PROVIDER_SCOPE` +- `EPP_OUTBOUND_CLIENT_ID` +- `EPP_OUTBOUND_MI_CLIENT_ID` -- [Soprano](docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) -- [Telesign](docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration) +The outbound managed identity first requests `api://AzureADTokenExchange/.default`. The configured +provider scope is used for the final token sent to `/messages/omnimsg`. The Function stores no +Soprano client secret and does not forward the incoming Microsoft Entra ID token. +Missing settings, blank tokens, and tokens with 30 seconds or less remaining fail before provider +HTTP. There is no API-key fallback or provider retry. + +For voice, the adapter extracts the first six consecutive digits from `message`, uses the request +locale or `en-US`, and sends `beforePasswordText`, `password`, and `afterPasswordText` in +`voice.text2voice`. A message without six consecutive digits fails before provider HTTP. + +### Telesign + +Telesign uses `telesign-customer-id` and `telesign-api-key` from Key Vault. Basic authentication is: + +```text +base64(UTF8(customer-id:api-key)) +``` + +The adapter sends `recipient.phone_number`, `message.text`, optional `message.language`, one +`channels[].channel`, and `correlation_id`. SMS forwards the message unchanged. Voice separates +standalone six-digit runs with commas and repeats the message twice. +`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`. + +Recognized success codes: + +- SMS: `200`, `203`, `290`, `291`, `292` +- Voice: `100`, `101`, `102`, `103` +- Either channel: `3001` on a successful HTTP response + +Unknown or malformed statuses fail closed. Acceptance does not confirm handset receipt or +completed audio. + +## Production readiness + +Before live use: + +- Keep Easy Auth enabled for every public `/api/SendOtp` route. +- Use managed identity for Key Vault access. +- Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, raw exceptions, + and provider responses out of logs. +- Validate evaluation, live acceptance, recipient delivery, caller rejection, invalid payloads, + and tampered JWE independently. +- Define end-to-end timing, duplicate-send protection, retry handling, certificate renewal, + rollback, monitoring, and delivery confirmation. +- Check a timeout with the provider before retrying. ## Security **Easy Auth is the only caller-authentication gate before the anonymous Function.** Follow the [exact trust checklist](docs/ONBOARDING.md#manual-deployment-trust-checklist) for HTTPS, -issuer, audience, caller allowlist, and unauthenticated rejection. Never disable it or exempt SendOtp; -there is no handler token validator or function-key fallback. +issuer, audience, caller allowlist, and unauthenticated rejection. Never disable it or exempt SendOtp. +There is no handler token validator or function-key fallback. JWE and its nonce prove no caller identity: anyone with the public key can encrypt. -Provider credentials authenticate outbound calls, not incoming requests; vault access uses managed identity. +Provider credentials authenticate outbound calls, not incoming requests. Vault access uses managed identity. Core Tools has no Easy Auth: bind locally to loopback without tunnels. [Deployed checks](docs/ONBOARDING.md#validate-the-deployed-endpoint) remain required after offline tests. @@ -210,11 +350,3 @@ For code work, keep the [HTTP contract](docs/CONTRACT.md) and [log schema](docs/ profile or an approved provider offer. - **New language**: mirror the folder structure, implement the contract, add the same test scenarios, and wire it into [.github/workflows/ci.yml](.github/workflows/ci.yml). - -### Future pull requests - -Start a short-lived branch from up-to-date `main`. After review and passing checks, select **Squash -and merge** to place one commit on `main`, then delete that PR's feature branch. Squashing is a merge -choice, not automatic just because commits are on a feature branch. Do not merge old feature histories -into a new branch or delete other branches containing unmerged work. This workflow does not rewrite -existing `main` history. diff --git a/docs/BRING-YOUR-OWN-PROVIDER.md b/docs/BRING-YOUR-OWN-PROVIDER.md index f089bc3..a104226 100644 --- a/docs/BRING-YOUR-OWN-PROVIDER.md +++ b/docs/BRING-YOUR-OWN-PROVIDER.md @@ -1,17 +1,29 @@ -# Bring your own provider +# Add Twilio + -[Documentation](README.md) | [Exact HTTP contract](CONTRACT.md) | [Runtime guides](README.md#develop) +[Documentation](README.md) | [Send OTP API](CONTRACT.md) | [Technical details](../TECHNICAL.md) -Connect a phone service to the **External Phone Provider (EPP)** contract. **Microsoft Entra ID** -sends an encrypted delivery request. The endpoint verifies, decrypts, evaluates, or sends it. +Use this guide to add a Twilio adapter to one runtime. -Confirm [eligibility, testing, and activation](ONBOARDING.md#before-purchasing-or-deploying) -before deployment. +> [!NOTE] +> Twilio is not included in the sample or configured by guided setup. Adding it requires code, +> tests, packaging, deployment settings, and your approved Twilio account configuration. -Read the [API contract](CONTRACT.md), then [add your integration](#where-to-add-your-integration) -and [validate it](#before-activation). + +## Before you start + +Prepare: + +- The [Send OTP API contract](CONTRACT.md). +- An approved Twilio account and API design for SMS and voice. +- One runtime: [JavaScript](../javascript/README.md), [C#](../dotnet/README.md), or + [Python](../python/README.md). +- Test credentials and endpoints that do not send to production recipients. + +Keep Easy Auth, envelope validation, JWE decryption, evaluation mode, and public responses in the +shared handler. The Twilio adapter should only build the outbound request and interpret the response. @@ -21,324 +33,107 @@ and [validate it](#before-activation). -## Understand the request - -The [Send OTP API reference](CONTRACT.md) is the single source for: - -- Authentication and headers. -- Envelope and JWE fields. -- Evaluation and live modes. -- Success and error responses. -- HTTP status codes. -- Runtime parsing differences. - -Provider code starts after caller authentication, envelope validation, and decryption. It receives -validated delivery data and returns a normalized provider result. - -Keep these boundaries: - -- Easy Auth verifies the caller. -- The shared handler validates and decrypts the request. -- The provider adapter builds the outbound request and interprets the response. -- The shared handler creates the response to Microsoft Entra ID. - -Do not forward the incoming bearer token. Do not return raw provider JSON. Echo the nonce only -after evaluation or recognized provider acceptance. - -## Where to add your integration - -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 the -incoming caller or write the HTTP response to Entra ID. + +## Add the adapter -### Provider request and response mapping - -Each provider implementation owns: - -- Its fixed provider ID and authentication mode. -- Its required credentials and settings. -- SMS and voice request construction. -- Response parsing and accepted-status rules. -- Safe failure classification. -The shared transport owns: +1. Use a fixed lowercase provider ID such as `twilio`. +2. Define the authentication mode and credential names. +3. Map SMS and voice requests to the approved Twilio API. +4. Map recognized Twilio responses to `Continue`, `Block`, or `Fail`. +5. Return the shared `ProviderResult`. +6. Keep sending in the shared runtime transport. -- HTTPS URL validation. -- Disabled redirects. -- The configured provider timeout. -- Response-body reading. -- Network and timeout errors. +Only a recognized accepted response can return success. A successful provider response does not +confirm recipient delivery. -Return a normalized result to the handler. Do not return raw provider JSON to Microsoft Entra ID -or write it to logs. The shared handler applies the -[HTTP status mapping](CONTRACT.md#2-outcome--http-status-mapping). +Do not forward the incoming Microsoft Entra ID bearer token or return raw Twilio JSON. -A provider `2xx` response is not enough unless the adapter recognizes its status as accepted. -Acceptance is not recipient delivery. -The shared outcomes are `Continue`, `Block`, and `Fail`. There is no `StepUp` or `409` mapping. -Use the existing safe failure reasons: `provider_http_error`, `provider_rejected`, -`unrecognized_provider_status`, `missing_provider_message_id`, and `invalid_provider_json`. + +## Register it -JavaScript and .NET pass cancellation through response reading. Python uses connect and read -inactivity timeouts. - - -### Provider settings +### JavaScript -#### Provider and endpoint - -- **`EPP_PROVIDER_NAME`** is the fixed ID of the registered adapter. -- **`EPP_PROVIDER_SMS_ENDPOINT`** is the required SMS URL for a live SMS request. -- **`EPP_PROVIDER_VOICE_ENDPOINT`** is the required voice URL for a live voice request. -- **`EPP_PROVIDER_ENDPOINT_REGION`** is the `global` or `eu` routing label from guided setup. -- **`EPP_PROVIDER_TIMEOUT_MS`** is the provider timeout in milliseconds. The default is `1500`. - The maximum is `2500`. -- **`EPP_PROVIDER_ACCOUNT_NAME`** is the sender or account value when required. - -#### Authentication - -- **`EPP_PROVIDER_AUTH_MODE`** is `apiKey` or `oauth`, matching the adapter. -- **`KEY_VAULT_URL`** identifies the vault containing API-key credentials. -- **`AZURE_CLIENT_ID`** selects a user-assigned identity for Key Vault when needed. -- **`EPP_PROVIDER_TENANT_ID`** identifies the provider tenant for OAuth. -- **`EPP_PROVIDER_SCOPE`** is the provider OAuth scope. -- **`EPP_OUTBOUND_CLIENT_ID`** identifies the client application used for OAuth exchange. -- **`EPP_OUTBOUND_MI_CLIENT_ID`** identifies the user-assigned identity used for OAuth exchange. +1. Add the adapter under [providers/](../javascript/src/functions/providers/). +2. Import it in [providers/index.js](../javascript/src/functions/providers/index.js). +3. Add its ID to `selectProvider` and export it. +4. Implement `name`, `authenticationMode`, `credentialSpec`, `createRequest`, and + `interpretResponse`. +5. Add tests under [javascript/test/](../javascript/test/). -#### Caching +### C# -- **`EPP_KEY_VAULT_CACHE_ENABLED`** controls API-key credential caching. -- **`EPP_ACCESS_TOKEN_CACHE_ENABLED`** controls OAuth credential caching. +1. Add the class under [Providers/](../dotnet/Src/Providers/). +2. Register it in [Program.cs](../dotnet/Program.cs) as a singleton `PhoneProviderBase`. +3. Implement `Name`, `AuthenticationMode`, `FetchCredentialsAsync`, and `SendOtpAsync`. +4. Use `SendJsonAsync` and return `ProviderResult`. +5. Add tests under [dotnet/tests/](../dotnet/tests/). -Each live request uses the endpoint for its selected channel. There is no shared endpoint fallback. -Endpoints must be absolute HTTPS URLs with a hostname, valid port, no user information, and no -fragment. Redirects are not followed. +### Python -Settings select registered code. They do not install an adapter. Evaluation does not require a -provider name, endpoint, or provider credential. +1. Add the class under [providers/](../python/src/providers/). +2. Add an instance to `_providers` in [function_app.py](../python/function_app.py). +3. Derive from `PhoneProviderBase`. +4. Implement `name`, `authentication_mode`, `credential_spec`, `build_request`, and `map_response`. +5. Return `OutboundRequest` and `ProviderResult`. +6. Add tests under [python/tests/](../python/tests/). -There is no default provider or automatic failover. The request body does not select a provider. -Telesign and Soprano use the selected URL unchanged. Infobip and Sinch append their API paths to -the selected base URL. The request channel selects the SMS or voice endpoint. -The legacy `EPP_PROVIDER_CHANNEL` setting is not used. + + + + +## Configure it + -### Authentication and credential caching - -Use managed identity to read provider credentials from Key Vault. Keep secrets out of code, -settings, logs, and documentation. -The default runtime resolver uses `ManagedIdentityCredential`, not the developer's CLI identity. - -For OAuth, exchange the outbound managed-identity assertion for the provider token. Do not use a -client-secret fallback and never forward the incoming Microsoft Entra ID token. - -Soprano uses `EPP_PROVIDER_AUTH_MODE=oauth`. With -`EPP_ACCESS_TOKEN_CACHE_ENABLED=true`, the runtime reuses its credential and SDK token caches. - -Cache settings accept trimmed, case-insensitive `true` or `false`. Unset values default to `true`. -Other values fail live credential acquisition without blocking evaluation. Restart workers after -a change. - -| Runtime | Enabled cache behavior | -| --- | --- | -| JavaScript | Warms at startup and polls every 30 seconds. OAuth uses `getToken`. | -| Python | Uses the same polling model. OAuth uses `get_token_info` when available, then `get_token`. | -| .NET | Warms once and refreshes on the next cache miss through `GetOrCreateAsync`. It has no polling loop. | - -With caching disabled, each live request obtains credentials for that request. There is no startup -preparation, polling, or cross-request reuse. Evaluation still skips provider credential work. -The platform's decryption-key reference is separate from these provider caches. - -Credential acquisition uses a 2.5-second budget in each runtime. That budget is not the total -Function deadline. Failed credential acquisition never sends an OTP. - -### Built-in provider notes - - -#### Soprano - -Soprano uses OAuth client-assertion exchange with: -- `EPP_PROVIDER_TENANT_ID` -- `EPP_PROVIDER_SCOPE` -- `EPP_OUTBOUND_CLIENT_ID` -- `EPP_OUTBOUND_MI_CLIENT_ID` +Set: -The endpoint app and outbound identity share a home tenant. The app must be multitenant and -authorized in the provider tenant. The federated credential uses the identity principal ID, -home-tenant v2 issuer, and `api://AzureADTokenExchange` audience. +- `EPP_PROVIDER_NAME=twilio` +- `EPP_PROVIDER_AUTH_MODE` to the mode implemented by the adapter. +- `EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT` for supported channels. +- Twilio credential references and any additional adapter settings. -The managed identity first requests `api://AzureADTokenExchange/.default`. The selected provider -scope is used for the final application token. The Function sends that token to the -`/messages/omnimsg` API and does not store a Soprano client secret. +Keep credentials in Key Vault and read them through managed identity. Use the shared cache and +timeout services instead of adding another credential cache or send-retry loop. -Only the final provider token is sent. The Function does not forward the managed-identity -assertion or incoming Microsoft Entra ID token. Missing settings, token failures, blank tokens, and -tokens with 30 seconds or less remaining fail before provider HTTP. There is no API-key fallback or -provider retry. The Function treats the token as opaque. The provider validates its claims. +See [provider settings and credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). -The selected SMS or voice URL is used unchanged. +## Test it -For voice, the adapter: +Cover: -1. Finds the first six consecutive digits in `message`, preserving leading zeros. -2. Splits the text into `beforePasswordText`, `password`, and `afterPasswordText`. -3. Uses the request locale or `en-US`. -4. Sends `gender: 1` and `loop: 2` in `voice.text2voice`. +- Registration and configuration. +- SMS and voice request mapping. +- Missing or invalid credentials. +- Accepted, rejected, unknown, and malformed responses. +- Network errors and timeouts. +- Evaluation mode with no provider call. +- Privacy-safe logs with no phone number, message, passcode, nonce, token, key, or raw response. -A message without six consecutive digits fails before provider HTTP. SMS forwards `message` -unchanged. Voice does not send a top-level `text` field. +Use fake credentials and service responses for offline tests. - -#### Telesign - -Telesign uses the complete configured SMS or voice URL. It sends: - -- `recipient.phone_number` -- `message.text` -- Optional `message.language` -- One `channels[].channel` -- `correlation_id` - -Responses use `reference_id`, `status.code`, and `status.description`. Only safe normalized fields -are logged. - -`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`, including the leading `+`. A nonblank locale -becomes `message.language`. `correlation_id` uses the request correlation ID or the generated -message ID. - -SMS forwards the message unchanged. Voice separates standalone six-digit runs with commas and -repeats the full message twice. - -Basic authentication uses: - -```text -base64(UTF8(customer-id:api-key)) -``` - -Set the header to `Authorization: Basic {encoded-value}`. Provider documentation can abbreviate -this as `Basic YOUR_API_KEY`. Do not send that text literally. - -Read `telesign-customer-id` and `telesign-api-key` from Key Vault. Digest and provider-token -authentication are not implemented. - -Recognized success codes are: - -- SMS: `200`, `203`, `290`, `291`, `292` -- Voice: `100`, `101`, `102`, `103` -- Either channel: `3001` on a successful HTTP response - -Unknown or malformed statuses fail closed. `3001` means the provider accepted the operation. It -does not confirm handset receipt or completed audio. - -The adapter does not send `account_lifecycle_event`, `originating_ip`, or `X-Shutter-Mode: true`. -The legacy `TELESIGN_VOICE` setting has no effect. -Use Function evaluation mode for a no-send endpoint test. - -### Add and register the provider - -Choose a fixed lowercase provider ID. - -#### JavaScript - -1. Add the module under [providers/](../javascript/src/functions/providers/). -2. Import it in [providers/index.js](../javascript/src/functions/providers/index.js), add its ID to - `selectProvider`, and export it. -3. Export `name`, `authenticationMode`, `credentialSpec`, `createRequest`, and `interpretResponse`. -4. Return [ProviderResult](../javascript/src/functions/providerResult.js). -5. Add tests under [javascript/test/](../javascript/test/). - -`createRequest` receives the delivery data, credentials, and `env`. Keep sending in the shared -[transport](../javascript/src/functions/providerTransport.js). - -#### C# - -1. Add the class under [Providers/](../dotnet/Src/Providers/). -2. Register it in [Program.cs](../dotnet/Program.cs) as a singleton `PhoneProviderBase`. -3. Implement `Name`, `AuthenticationMode`, `FetchCredentialsAsync`, and `SendOtpAsync`. -4. Use `SendJsonAsync` and return [ProviderResult](../dotnet/Src/Models.cs). -5. Add tests under [dotnet/tests/](../dotnet/tests/). - -Use `IEnv` for settings and [ISecretResolver](../dotnet/Src/ISecretResolver.cs) for vault secrets. -Return credentials with a valid `ExpiresOn` and honor cancellation. - -#### Python - -1. Add the class under [providers/](../python/src/providers/). -2. Add an instance to the `_providers` tuple in [function_app.py](../python/function_app.py). -3. Derive from [PhoneProviderBase](../python/src/provider.py). -4. Implement `name`, `authentication_mode`, `credential_spec`, `build_request`, and `map_response`. -5. Return `OutboundRequest` and [ProviderResult](../python/src/models.py). -6. Add tests under [python/tests/](../python/tests/). - -Use the supplied `env` for extra settings. Keep sending in the base `send_otp` transport. - -Your hooks own service-specific mapping, outbound authentication, and recognition of acceptance -or failure. Keep credential caching in the existing services: -[JavaScript credentials.js](../javascript/src/functions/credentials.js), -[C# CredentialTokenService](../dotnet/Src/CredentialTokenService.cs), and -[Python credentials.py](../python/src/credentials.py). JavaScript/Python resolve the provider's -credential specification. C# calls `FetchCredentialsAsync`. Do not add a separate credential cache -or send-retry loop. Keep service secrets in Key Vault, not in settings or source. + + +## Package and deploy -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). +1. Run the selected runtime tests. +2. [Build a new package](../TECHNICAL.md#build-zips-locally). +3. Follow [manual deployment](ONBOARDING.md#optional-local-development-and-manual-deployment). +4. Configure Easy Auth, app settings, Key Vault access, and monitoring. +5. Run the [approved endpoint checks](ONBOARDING.md#validate-the-deployed-endpoint). - -### Implementation steps - -1. Start from [local.settings.sample.json](local.settings.sample.json). Copy it to an untracked - `local.settings.json` beside your runtime's `host.json`, keep all `Values` entries as strings, - and select `node`, `dotnet-isolated`, or `python` as `FUNCTIONS_WORKER_RUNTIME`. - Use a local test decryption key, never a production key. -2. Set `EPP_PROVIDER_NAME` to your registered ID and `EPP_PROVIDER_AUTH_MODE` to its matching - `apiKey` or `oauth` mode. Supply `EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT` - for the channels you support, plus the credential and extra settings your code reads. - Keep these aligned with [JavaScript config.js](../javascript/src/functions/config.js), - [C# AppConfig](../dotnet/Src/AppConfig.cs), or [Python config.py](../python/src/config.py) and the - [provider settings](#provider-settings). - **Settings select registered code. They do not create or register an adapter.** -3. Add tests in the directory above for registration, request mapping, missing credentials, - recognized acceptance, rejection, unknown responses, and timeouts on each supported channel. - Check that evaluation sends nothing, live success waits for acceptance, and failures omit the - nonce. Use fake credentials and service responses. Run your runtime's tests and build. -4. [Package your changed source](../TECHNICAL.md#build-zips-locally), not an unchanged public - release. C# needs publish output. Python needs the documented Linux remote build. - Follow [manual deployment](ONBOARDING.md#optional-local-development-and-manual-deployment) - to deploy the custom package and apply cloud settings and vault permissions separately. - Keep the [Easy Auth trust checks](ONBOARDING.md#manual-deployment-trust-checklist), then - complete the validation below. +Do not deploy an unchanged public release after adding the adapter. + + -## Before activation +## Before live use - +Confirm authentication, evaluation, controlled live acceptance, recipient delivery, monitoring, +renewal, rollback, retry handling, and duplicate-send protection. -Run [authorized endpoint checks](ONBOARDING.md#validate-the-deployed-endpoint): rejected callers, -invalid or tampered payloads, evaluation without a send, and controlled live tests for each channel. -Check handset receipt or voice audio separately from acceptance. For local tests, use test keys -and fake service responses on loopback only, without tunnels. Core Tools has no Easy Auth. - -The sample completes the request after provider acceptance. Plan any additional production -capabilities your design needs: - -- Durable queues or background delivery. -- Duplicate-send protection and safe retries. -- End-to-end timing for cold starts, Key Vault, and provider calls. -- Overlapping encryption-key rotation. -- E.164 and body-size validation. -- Provider delivery confirmation. -- Voice locale, passcode, and audio validation. - -The outbound timeout covers only the provider call. Check a timed-out send before retrying because -the provider may already have accepted it. - -Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, and raw service -responses out of logs. Disable body tracing in the application, platform, and proxies. -Complete setup certification, provider account checks, monitoring, renewal, rollback, and the -approved Microsoft activation procedure before live use. +See [production readiness](../TECHNICAL.md#production-readiness). diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md index cc8e803..3715d12 100644 --- a/docs/CONTRACT.md +++ b/docs/CONTRACT.md @@ -1,20 +1,35 @@ -# Send OTP API +# Send OTP + -[Documentation](README.md) | [Build a provider integration](BRING-YOUR-OWN-PROVIDER.md) | [Settings and packages](../TECHNICAL.md) +[Documentation](README.md) | [Technical details](../TECHNICAL.md) | [Add Twilio](BRING-YOUR-OWN-PROVIDER.md) -Use this API to receive an encrypted SMS or voice request from **Microsoft Entra ID**. +Receive an encrypted SMS or voice request from **Microsoft Entra ID**. | | | | --- | --- | | **Method** | `POST` | | **Path** | `/api/SendOtp` | | **Authentication** | Easy Auth | -| **Function trigger** | `authLevel: anonymous` | | **Content type** | `application/json` | -| **Success** | `200 OK` with the decrypted nonce | + +> [!IMPORTANT] +> Easy Auth must validate the approved Microsoft caller before the Function runs. The encrypted +> payload protects request data. It does not authenticate the caller. + +## Authentication + +Configure Easy Auth with: + +- HTTPS required. +- Unauthenticated requests returned as `401`. +- The endpoint application audience. +- The trusted tenant issuer. +- A nonempty allowlist for the approved Microsoft caller. + +See the [Easy Auth checklist](ONBOARDING.md#manual-deployment-trust-checklist). ## HTTP request @@ -23,121 +38,73 @@ Use this API to receive an encrypted SMS or voice request from **Microsoft Entra ```http POST https:///api/SendOtp Content-Type: application/json -Authorization: Bearer {access-token} +Authorization: Bearer ``` -Easy Auth validates the bearer token before the Function runs. Configure the trusted issuer, -endpoint audience, and authorized Microsoft caller by following the -[Easy Auth checklist](ONBOARDING.md#manual-deployment-trust-checklist). - -The JWE protects the request data. It does not authenticate the caller. - -Easy Auth must use `requireAuthentication=true`, `unauthenticatedClientAction=Return401`, -`requireHttps=true`, the endpoint audience in `allowedAudiences`, and a nonempty Microsoft caller -allowlist. - ## Request headers -| Header | Use | +| Header | Value | | --- | --- | -| `Authorization` | **Required.** Bearer token consumed by Easy Auth. The Function does not parse or forward it. | -| `Content-Type` | **Required.** Must be `application/json`. | -| `x-ms-correlation-id` | Optional correlation fallback when the body does not contain `correlationId`. | -| `x-ms-client-request-id` | Optional per-attempt identifier used internally as `messageId`. | -| `User-Agent` | Optional. Not interpreted or logged by the sample. | +| `Authorization` | Required bearer token. Easy Auth consumes it. | +| `Content-Type` | Required. Use `application/json`. | +| `x-ms-correlation-id` | Optional correlation fallback. | +| `x-ms-client-request-id` | Optional request identifier. | -Forwarded platform headers, including `x-ms-client-principal`, are context only. +The Function does not forward the bearer token to the phone provider. ## Request body -| Field | Description | -| --- | --- | -| `type` | **Required string.** Must be `microsoft.mfa.otpDeliver.v1`. | -| `channel` | **Required integer or string.** `1` or `"sms"` for SMS. `2` or `"voice"` for voice. Names are case-insensitive. | -| `mode` | **Required integer or string.** `1` or `"live"` for delivery. `2` or `"evaluation"` for no-send evaluation. Names are case-insensitive. | -| `encryptedDeliveryContext` | **Required string.** Compact JWE containing the delivery data. | -| `tenantId` | Optional string with routing metadata. It does not select authentication or a provider. | -| `correlationId` | Optional sign-in correlation string. | -| `ttlSeconds` | Optional positive JSON integer up to `2147483647`. | - -Use an integer such as `60` for `ttlSeconds`. Do not send `"60"`, `60.0`, `6e1`, `null`, a -boolean, or a nonpositive value. TTL validation does not enforce passcode expiry or a delivery -deadline. +- **`type`** — Required string. Use `microsoft.mfa.otpDeliver.v1`. +- **`channel`** — Required integer or string. `1` or `"sms"` for SMS. `2` or `"voice"` for voice. +- **`mode`** — Required integer or string. `1` or `"live"` to send. `2` or `"evaluation"` + for no-send validation. +- **`encryptedDeliveryContext`** — Required compact JWE string. +- **`tenantId`** — Optional routing metadata. +- **`correlationId`** — Optional sign-in correlation value. +- **`ttlSeconds`** — Optional positive integer up to `2147483647`. -Unknown versions, unsupported channel or mode values, invalid TTL values, and missing encrypted -content return `400 Bad Request`. +Send integer channel, mode, and TTL values in their documented forms for consistent behavior across +all runtimes. See [request validation and runtime differences](../TECHNICAL.md#request-validation-and-runtime-differences). ### Encrypted delivery context -The compact JWE must: - -- Use `RSA-OAEP-256` for key wrapping. -- Use `A256GCM` for content encryption. -- Contain exactly five nonempty segments. -- Be no longer than 16,384 characters. -- Pass authentication-tag validation before the plaintext is used. - -Pass the original compact JWE to the JOSE library. Do not rebuild its protected header. -The protected `alg` and `enc` values are fixed. `RSA-OAEP`, `A128GCM`, and -`A256CBC-HS512` are rejected. - -The sample decrypts with the single RSA private key in `EPP_DECRYPTION_KEY_PEM`. In Azure, use a -Key Vault secret reference. `EPP_ENCRYPTION_KEY_ID` checks the JWE `kid` after decryption and logs -only `encryption_key_id_mismatch` on a mismatch. It does not select a key. +The JWE uses `RSA-OAEP-256` and `A256GCM`. After decryption, the JSON contains: -See [certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle) for renewal -and key synchronization. - -The decrypted JSON contains: - -| Field | Description | -| --- | --- | -| `nonce` | **Required string.** Returned unchanged after evaluation or provider acceptance. | -| `phoneNumber` | **Required string.** Expected in E.164 form. | -| `message` | **Required string.** Localized message containing the passcode. | -| `locale` | Optional voice locale when supported by the provider. | -| `extension` | Optional office-voice data. The shared dispatcher does not forward it. | -| `riskContext` | Optional context data. The sample does not evaluate it. | -| `textToVoice` | Optional legacy speech data. | +- **`nonce`** — Required. Returned unchanged after evaluation or provider acceptance. +- **`phoneNumber`** — Required phone number. +- **`message`** — Required localized message containing the passcode. +- **`locale`** — Optional voice locale. +- **`extension`** — Optional office-voice data. +- **`riskContext`** — Optional context data. +- **`textToVoice`** — Optional legacy speech data. -`nonce`, `phoneNumber`, and `message` must be nonblank strings. Decryption or completeness -failures return `400 Bad Request`. -The nonce confirms decryption in the response. It is not caller authentication or replay protection. - -Provider-specific message mapping belongs in the -[provider implementation guide](BRING-YOUR-OWN-PROVIDER.md#provider-request-and-response-mapping). +See [JWE validation and key handling](../TECHNICAL.md#jwe-validation-and-key-handling) for limits, +algorithms, key settings, and runtime behavior. ## Modes + + -### Evaluation - -Use `mode: 2` or `"evaluation"` to validate authentication, envelope parsing, and JWE decryption. -The Function returns the nonce without selecting a provider, reading provider credentials, or -sending a message. - -Easy Auth and the decryption-key reference still apply in Azure. Core Tools does not provide Easy -Auth, so local evaluation must stay on loopback without tunnels. - -### Live - -Use `mode: 1` or `"live"` to send through the configured provider. The Function returns success -only after the adapter recognizes provider acceptance. +- **Evaluation:** validates authentication, the envelope, and decryption. It returns the nonce + without reading provider credentials or sending a message. +- **Live:** sends through the configured provider and returns success after recognized provider + acceptance. -Provider acceptance is not proof of recipient delivery. Check delivery through the provider's -supported evidence. +Provider acceptance is not proof of recipient delivery. ## Response + -### Success response +If successful, the endpoint returns `200 OK`: ```http HTTP/1.1 200 OK @@ -152,126 +119,51 @@ Content-Type: application/json } ``` -The response contains: - | Field | Description | | --- | --- | | `nonce` | Original decrypted nonce. | -| `correlationId` | Body value, then `x-ms-correlation-id`, then a generated request ID. | -| `providerStatus` | Always `"accepted"` for evaluation or recognized live acceptance. | - -Failures omit `nonce` and `providerStatus`. Handler errors include a generated `requestId` and -may include a fixed `reason` and `correlationId`. Platform authentication and .NET host-binding -errors can occur before this JSON response is created. - -The caller uses a successful response with the matching nonce as the handled result. This -repository does not implement or guarantee caller fallback, retry, or timing behavior. Validate -those behaviors through the approved EPP procedure. - -Handler failures use `error: "bad_request"` or `error: "decryption_failed"`. Fixed validation -reasons include: - -- `invalid JSON body` -- `invalid envelope` -- `invalid payload` -- `unsupported envelope type` -- `unsupported payload type` -- `encryptedDeliveryContext is required` -- `unsupported channel` -- `unsupported mode` -- `invalid ttlSeconds` -- `ttlSeconds expired` -- `incomplete delivery context` +| `correlationId` | Request correlation value or generated fallback. | +| `providerStatus` | `"accepted"` for evaluation or recognized live acceptance. | + +Failures omit `nonce` and `providerStatus`. ## HTTP status codes -| Status | Meaning | +| Status | Description | | --- | --- | -| `200 OK` | Evaluation completed or the provider returned a recognized accepted status. | -| `400 Bad Request` | Invalid JSON, envelope, JWE, delivery context, or provider request. | -| `401 Unauthorized` | Easy Auth rejected the caller, or the provider rejected its credentials. | -| `403 Forbidden` | Easy Auth denied the caller, or the provider returned a recognized block. | -| `429 Too Many Requests` | The provider returned `429`. | -| `502 Bad Gateway` | Missing provider configuration, network failure, or unrecognized provider response. | -| `504 Gateway Timeout` | The provider request timed out. | +| `200 OK` | Evaluation completed or the provider accepted the request. | +| `400 Bad Request` | Invalid request or encrypted content. | +| `401 Unauthorized` | Caller or provider authentication failed. | +| `403 Forbidden` | Caller access was denied or the provider blocked the request. | +| `429 Too Many Requests` | Provider throttling. | +| `502 Bad Gateway` | Provider configuration, network, or response failure. | +| `504 Gateway Timeout` | Provider request timed out. | -Unknown, incomplete, or unsuccessful provider responses fail closed. A provider `2xx` response is -not enough unless the adapter recognizes its status as accepted. +Check a timed-out live request before retrying. The provider might already have accepted it. -A timed-out live request might already have reached the provider. Check its outcome before retrying. - -## Runtime notes +## Related guidance - -Use the request forms above for every runtime. - -- **Parsing:** JavaScript and Python parse JSON inside the handler. .NET binds to - `EntraSendOtpPayload` first, so malformed JSON or incompatible types can fail before the normal - error body is created. -- **Channel and mode:** .NET can bind numeric strings such as `"1"`. JavaScript and Python reject - them. Use integer codes or the documented names. -- **TTL:** JavaScript normalizes `60.0` and `6e1` to `60`. Python and .NET reject those forms. - .NET treats `null` as omitted. Send an integer or omit the field. -- **Validation:** .NET uses `unsupported payload type` where JavaScript and Python use - `unsupported envelope type`. A null .NET payload returns `invalid payload`. Incompatible - decrypted field types can return `decryption_failed` in .NET before its completeness check. -- **Delivery context:** .NET models `locale`. JavaScript and Python also retain `extension` and - `riskContext`. Shared dispatch does not forward the latter fields. -- **Correlation:** normal string values use the body, then `x-ms-correlation-id`, then a generated - ID. JavaScript uses the raw header. Python and .NET sanitize it. .NET preserves an empty body - value while JavaScript and Python fall back. Early validation errors omit correlation. -- **Request ID:** `x-ms-client-request-id` supplies `messageId`. JavaScript uses the raw header. - Python and .NET sanitize it. Request and correlation IDs do not provide deduplication. - -See the runtime handlers for exact binding behavior: -[JavaScript](../javascript/src/functions/SendOtp.js), -[Python](../python/function_app.py), and -[.NET](../dotnet/Functions/SendOtp.cs). - -## Provider implementation - + + - -The HTTP contract is provider-neutral. Use -[Bring your own provider](BRING-YOUR-OWN-PROVIDER.md) for: - -- Provider request and response mapping. -- Settings and channel endpoints. -- API-key and OAuth authentication. -- Credential caching and refresh. -- Soprano and Telesign behavior. -- Packaging, tests, and production planning. - -App settings keep the `EPP_` prefix. Shared code uses the `Envelope` and `DeliveryContext` models. - -## Required behavior - + -- Keep Easy Auth enabled for every public `/api/SendOtp` route. -- Use managed identity for Key Vault access. -- Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, raw exceptions, - and provider responses out of logs. -- Do not forward the incoming bearer token to a provider. -- Do not retry an uncertain live send without checking the provider outcome. -- Test evaluation, live acceptance, recipient delivery, caller rejection, invalid payloads, and - tampered JWE independently. - -See: - +- [Technical implementation details](../TECHNICAL.md#http-implementation-details) +- [Built-in providers](../TECHNICAL.md#built-in-providers) +- [Add Twilio](BRING-YOUR-OWN-PROVIDER.md) - [Application log reference](LOGGING.md) -- [Provider tests and production readiness](BRING-YOUR-OWN-PROVIDER.md#before-activation) - [Deployment validation](ONBOARDING.md#validate-the-deployed-endpoint) diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 019a1d7..9c07668 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -44,7 +44,7 @@ Prepare: Check the [full prerequisites](../setup/docs/README.md#prerequisites-for-step-2), including the Microsoft Entra ID allowed-tenants preview. Setup can install Bicep, but not Azure CLI. -Assign an **onboarding owner** and review [production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness). +Assign an **onboarding owner** and review [production readiness](../TECHNICAL.md#production-readiness). ## 2. Register your application diff --git a/docs/MONITORING.md b/docs/MONITORING.md index e030ba2..f92ba83 100644 --- a/docs/MONITORING.md +++ b/docs/MONITORING.md @@ -392,7 +392,7 @@ See [event order and identifier provenance](LOGGING.md). `credential_refresh_failed` also differs operationally: JavaScript/Python have a polling refresh loop, while .NET emits this event on failed startup/cache-fill acquisition and has no -periodic poller. See [credential caching](BRING-YOUR-OWN-PROVIDER.md#credential-caching). +periodic poller. See [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). Absence of a warning, especially in an idle app or a missing export pipeline, is not a credential health check. diff --git a/docs/MULTI-PROVIDER-IMPLEMENTATION.md b/docs/MULTI-PROVIDER-IMPLEMENTATION.md index 9f5faa2..459d25f 100644 --- a/docs/MULTI-PROVIDER-IMPLEMENTATION.md +++ b/docs/MULTI-PROVIDER-IMPLEMENTATION.md @@ -2,7 +2,7 @@ -[Documentation](README.md) | [Normal deployment](GETTING-STARTED.md) | [Provider interface](BRING-YOUR-OWN-PROVIDER.md) +[Documentation](README.md) | [Normal deployment](GETTING-STARTED.md) | [Provider interface](../TECHNICAL.md#provider-result-mapping) Add controlled primary-to-secondary fallback around `/api/SendOtp`. Keep one server-owned SMS provider order with durable state and provider-specific approval. @@ -153,7 +153,7 @@ module.exports = { stopProviderCredentialRefresh }; Remove the singleton import only after replacing its request/lifecycle uses. With its selected cache enabled, each service begins periodic refresh when first used. With caching disabled, each live attempt acquires credentials without a poller or cross-request reuse. Preserve -the [cache switches](BRING-YOUR-OWN-PROVIDER.md#credential-caching) in each account's configuration. +the [cache switches](../TECHNICAL.md#provider-settings-and-credential-caching) in each account's configuration. Setup sets them to `false` for FC1 and `true` for EP1. Omitted values default to `true`. An evaluation request must not start credential acquisition. Already-running refresh is independent. Rebuild contexts on controlled restart when configuration changes. Never repurpose a live service diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 8a6959b..59834cf 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -28,7 +28,7 @@ missing access. Azure subscription ownership does not grant EPP access. Assign an **onboarding owner** to coordinate tenant policy, Azure, the provider, and Microsoft support. -Use [production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness) to plan the +Use [production readiness](../TECHNICAL.md#production-readiness) to plan the capabilities your live service needs, such as durable delivery, retry and deduplication strategy, an end-to-end deadline, certificate renewal, and delivery confirmation. @@ -147,7 +147,7 @@ setup-created encryption certificate unchanged. 3. Complete the [authorized evaluation and live checks](#validate-the-deployed-endpoint). Evaluation does not validate these secrets. Setup disables provider caching for FC1 and enables it for EP1. Plan first-use and rotation checks from - [credential caching](BRING-YOUR-OWN-PROVIDER.md#credential-caching). + [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). No warning is not proof of account authorization. Use portal secret entry rather than command-line literal values, transcripts, source files, diff --git a/docs/README.md b/docs/README.md index c6f267f..e614040 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,60 +1,48 @@ -# Documentation by task +# Documentation -[Sample overview and architecture](../README.md) + -**First deployment:** follow [Get started](GETTING-STARTED.md). Confirm access and Microsoft's -approved test and activation process before deploying in a nonproduction environment. +[Sample overview](../README.md) | [Get started](GETTING-STARTED.md) -## Table of Contents +Use the guide that matches your task. -- [Understand the sample](#understand-the-sample) -- [Deploy and validate](#deploy-and-validate) -- [Operate](#operate) -- [Develop](#develop) -- [Advanced topics](#advanced-topics) -- [Get help](#get-help) + + +## Deploy -## Understand the sample - -- [Architecture and implementation](../TECHNICAL.md) -- [HTTP contract](CONTRACT.md) -- [Log schema](LOGGING.md) -- [Production readiness](BRING-YOUR-OWN-PROVIDER.md#production-readiness) +- [Five-step setup](GETTING-STARTED.md) +- [Configuration and validation](ONBOARDING.md) +- [Setup reference](../setup/docs/README.md) +- [Troubleshooting](../setup/docs/Troubleshooting.md) +- [Cost estimate](COST-CALCULATOR.md) -## Deploy and validate + + +## Understand and develop -| Task | Guide | -| --- | --- | -| Register an app, deploy, test, and hand over | [Five-step setup](GETTING-STARTED.md) | -| Find IDs and complete provider authentication | [Configuration and validation](ONBOARDING.md) | -| Check permissions, script choices, or deployment output | [Setup reference](../setup/docs/README.md) | -| Resolve a setup or endpoint failure | [Troubleshooting](../setup/docs/Troubleshooting.md) | -| Estimate SMS/voice provider charges | [Cost estimate](COST-CALCULATOR.md); not a billing report | +- [Architecture, settings, and built-in providers](../TECHNICAL.md) +- [Send OTP API](CONTRACT.md) +- Runtime guides: [JavaScript](../javascript/README.md), [C#](../dotnet/README.md), and + [Python](../python/README.md) +- [Add Twilio](BRING-YOUR-OWN-PROVIDER.md) +- [Application log fields](LOGGING.md) ## Operate -- [Function logs and alerts](MONITORING.md) +- [Function monitoring and alerts](MONITORING.md) - [Application Insights collection](APPLICATION-INSIGHTS.md) - [Microsoft Entra ID monitoring](SIGN-IN-MONITORING.md) - [Certificate renewal](../setup/docs/README.md#encryption-certificate-lifecycle) - [Rollback and decommissioning](../setup/docs/README.md#rollback-and-decommissioning) -## Develop - -- Runtimes: [JavaScript](../javascript/README.md), [C#](../dotnet/README.md), [Python](../python/README.md) -- [Settings and packages](../TECHNICAL.md) -- [HTTP contract](CONTRACT.md) -- [Log schema](LOGGING.md) -- [Phone-service integration](BRING-YOUR-OWN-PROVIDER.md) - ## Advanced topics - [Multi-region routing](FRONTDOOR.md) - [Provider fallback](MULTI-PROVIDER-IMPLEMENTATION.md) -## Get help + +## Support - [Provider access, testing, and activation](ONBOARDING.md#before-purchasing-or-deploying) -- [Troubleshooting](../setup/docs/Troubleshooting.md) - [Safe diagnostic evidence](../setup/docs/Troubleshooting.md#after-deployment-triage) diff --git a/dotnet/README.md b/dotnet/README.md index 991b319..0b1ddd4 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -69,21 +69,20 @@ does not echo it. **.NET binding runs first:** `[FromBody]` creates `EntraSendOtpPayload`. Malformed JSON, invalid enum tokens, or other deserialization errors can fail before `SendOtp`, its `ILogger` events, or its custom error response. Successfully bound requests retain application validation, -privacy-safe logging, and response behavior. See [binding differences](../docs/CONTRACT.md#runtime-binding-differences). +privacy-safe logging, and response behavior. See +[binding differences](../TECHNICAL.md#request-validation-and-runtime-differences). Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate authentication and decryption without a provider send. Live success confirms acceptance, **not handset receipt**. Check a timeout before retrying. -For message/voice mapping, see the [Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) -and [Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). +For built-in message and voice mapping, see +[Soprano and Telesign](../TECHNICAL.md#built-in-providers). ## Source -Use the provider-neutral -[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload, -caller authentication, and response rules. Customers can connect the outbound phone service -that meets their requirements. +To add Twilio, follow the [Twilio integration guide](../docs/BRING-YOUR-OWN-PROVIDER.md). +Keep caller authentication and public responses in the shared handler. For deployed diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and [runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery). @@ -99,7 +98,7 @@ accounts require a separate trust, configuration, credential, and test design. switches default to `true`; disabling the selected switch skips warmup and fetches on every live request. Enabled credentials remain in `MemoryCache` until absolute expiry. There is no polling timer, and concurrent misses can fetch more than once. The fetch has a 2.5-second cancellation -budget. See [credential caching](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). +budget. See [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). | Source | Purpose | |---|---| @@ -122,4 +121,4 @@ method delegates to the base `SendJsonAsync` transport and maps the t `ProviderResult`; provider mapping code never handles `HttpResponseMessage` or raw JSON DOM types. Each provider keeps its own response rules. Common async HTTP transport lives in `PhoneProviderBase`. See -[production readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. +[production readiness](../TECHNICAL.md#production-readiness) when planning a live service. diff --git a/javascript/README.md b/javascript/README.md index 64e10a8..acceb5f 100644 --- a/javascript/README.md +++ b/javascript/README.md @@ -69,27 +69,24 @@ Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-sen authentication and decryption without a provider send. Live success confirms acceptance, **not handset receipt**. Check a timeout before retrying. -For message/voice mapping and credential rules, see the -[Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) and -[Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). +For built-in message and voice mapping, see +[Soprano and Telesign](../TECHNICAL.md#built-in-providers). ## Source and extension points -Use the provider-neutral -[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload, -caller authentication, and response rules. Customers can connect the outbound phone service -that meets their requirements. +To add Twilio, follow the [Twilio integration guide](../docs/BRING-YOUR-OWN-PROVIDER.md). +Keep caller authentication and public responses in the shared handler. For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and the [JavaScript event queries](../docs/MONITORING.md#4-javascript-service-event-queries). -The [cache switches and defaults](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) apply here. +The [cache switches and defaults](../TECHNICAL.md#provider-settings-and-credential-caching) apply here. Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. With caching enabled, API keys use Key Vault and `lru-cache`; OAuth uses the managed-identity/Entra SDKs. A 30-second refresh loop and one in-flight acquisition support warm reads. See the -[credential caching guide](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). +[credential caching guide](../TECHNICAL.md#provider-settings-and-credential-caching). | Source | Purpose | |---|---| @@ -110,4 +107,4 @@ To add a provider, implement its fixed `name`, `authenticationMode`, `credential [providers/index.js](src/functions/providers/index.js). `interpretResponse` returns a `ProviderResult` containing the provider-owned outcome, endpoint HTTP status and one safe fixed failure classification. Raw API JSON stays inside the provider. See -[production readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. +[production readiness](../TECHNICAL.md#production-readiness) when planning a live service. diff --git a/python/README.md b/python/README.md index 179e4a0..650674a 100644 --- a/python/README.md +++ b/python/README.md @@ -69,27 +69,25 @@ Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-sen authentication and decryption without a provider send. Live success confirms acceptance, **not handset receipt**. Check a timeout before retrying. -For message/voice mapping, see the [Soprano integration](../docs/BRING-YOUR-OWN-PROVIDER.md#soprano-integration) -and [Telesign integration](../docs/BRING-YOUR-OWN-PROVIDER.md#telesign-integration). +For built-in message and voice mapping, see +[Soprano and Telesign](../TECHNICAL.md#built-in-providers). ## Source -Use the provider-neutral -[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload, -caller authentication, and response rules. Customers can connect the outbound phone service -that meets their requirements. +To add Twilio, follow the [Twilio integration guide](../docs/BRING-YOUR-OWN-PROVIDER.md). +Keep caller authentication and public responses in the shared handler. For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and [runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery). Verify which Python logging properties the host/export pipeline preserves. -The [cache switches and defaults](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) apply here. +The [cache switches and defaults](../TECHNICAL.md#provider-settings-and-credential-caching) apply here. Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. With caching enabled, API keys use Key Vault and `cachetools.TTLCache`; OAuth uses the managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the -[credential caching guide](../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching). +[credential caching guide](../TECHNICAL.md#provider-settings-and-credential-caching). | Source | Purpose | |---|---| @@ -106,4 +104,4 @@ managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the 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 readiness](../docs/BRING-YOUR-OWN-PROVIDER.md#production-readiness) when planning a live service. +See [production readiness](../TECHNICAL.md#production-readiness) when planning a live service. diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md index 9b04c32..c86b68a 100644 --- a/setup/docs/Troubleshooting.md +++ b/setup/docs/Troubleshooting.md @@ -141,7 +141,7 @@ It does not verify publication, startup, Easy Auth, or delivery. **Check:** serving-app `EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED`. Setup writes both as `"false"` for FC1 or `"true"` for EP1 and restores them on rerun. **Fix:** deploy a supporting release and restart after edits; older packages ignore the switches. -The [credential caching guide](../../docs/BRING-YOUR-OWN-PROVIDER.md#credential-caching) defines accepted values, +The [credential caching guide](../../TECHNICAL.md#provider-settings-and-credential-caching) defines accepted values, fail-closed invalid settings, and runtime differences. Disabling a cache does not eliminate live credential acquisition or affect the platform's decryption-key reference. From 77fd7f1019df64511b572536e9f04b541d499c1a Mon Sep 17 00:00:00 2001 From: Hou Chi Chan Date: Thu, 15 Oct 2026 16:06:41 -0700 Subject: [PATCH 3/5] docs: simplify operational guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe53050-0d2b-4d05-bcc1-20f5b611b665 --- docs/APPLICATION-INSIGHTS.md | 329 +++++------- docs/COST-CALCULATOR.md | 55 +- docs/FRONTDOOR.md | 86 ++- docs/GETTING-STARTED.md | 57 +- docs/LOGGING.md | 38 +- docs/MONITORING.md | 683 +++++++---------------- docs/MULTI-PROVIDER-IMPLEMENTATION.md | 32 +- docs/ONBOARDING.md | 420 +++++++-------- docs/README.md | 17 +- docs/SIGN-IN-MONITORING.md | 743 ++++++++------------------ 10 files changed, 882 insertions(+), 1578 deletions(-) diff --git a/docs/APPLICATION-INSIGHTS.md b/docs/APPLICATION-INSIGHTS.md index 8ed5f79..699d959 100644 --- a/docs/APPLICATION-INSIGHTS.md +++ b/docs/APPLICATION-INSIGHTS.md @@ -2,182 +2,144 @@ -[Documentation](README.md) | [Monitoring walkthrough](MONITORING.md) | [Log schema](LOGGING.md) +[Documentation](README.md) | [Monitoring](MONITORING.md) | [Log fields](LOGGING.md) -Use the [monitoring walkthrough](MONITORING.md) for queries and alerts. Use -[single-region configuration](#single-region-configuration) for missing telemetry. +Use this page when telemetry is missing, incomplete, or unclear. For everyday queries and alerts, +start with [monitoring](MONITORING.md). -Function telemetry covers execution, timing, and logs. Provider delivery evidence and -[Microsoft Entra ID sign-in monitoring](SIGN-IN-MONITORING.md) are separate. +| Task | Go to | +| --- | --- | +| Understand where records come from | [Collection](#collection) | +| Choose the correct table | [Signals and tables](#signals-and-tables) | +| Check one deployment | [Single region](#single-region) | +| Check several regions | [Multiple regions](#multiple-regions) | +| Diagnose missing data | [Coverage checks](#coverage-checks) | +| Review privacy and sampling | [Limits](#limits) | + +Function telemetry covers execution, timing, and logs. Microsoft Entra sign-ins and provider +delivery evidence are separate. + + +## Collection -Sections: [collection pipelines](#how-telemetry-reaches-application-insights), -[signals and tables](#signals-tables-and-metrics-are-different-things), -[single-region checks](#single-region-configuration), [ingestion identity](#identity-and-ingestion), -[multi-region layout](#multi-region-configuration), [coverage queries](#read-only-queries-to-establish-what-is-present), -or [sampling/privacy limits](#sampling-privacy-and-collection-limits). +The **Functions host** records invocation telemetry. The **language worker** runs the endpoint and +writes application logs. A worker exporter needs its own initialization, authentication, filters, +and sampling. -## How telemetry reaches Application Insights +Application Insights stores data in its linked Log Analytics workspace. Correlation can connect +requests, dependencies, exceptions, and traces when the active pipeline exports them. -The **Functions host** records invocation telemetry; the **language worker** runs endpoint code. -A direct worker exporter needs separate initialization, authentication, filtering, and sampling. +### Repository defaults -Application Insights sends records to its linked Log Analytics workspace. Correlation context -connects requests, dependencies, and logs. +| Runtime | Checked-in configuration | Verify after deployment | +| --- | --- | --- | +| [.NET](../dotnet/host.json) | Host uses `telemetryMode: OpenTelemetry`. The project references Functions OpenTelemetry and Azure Monitor packages. | `Program.cs` does not register a separate worker exporter. Confirm which worker logs, dependencies, and exceptions are present. | +| [JavaScript](../javascript/host.json) | Standard host Application Insights settings. Requests are excluded from sampling. | There is no direct Application Insights or OpenTelemetry exporter in the package manifest. Confirm outbound dependency coverage. | +| [Python](../python/host.json) | Standard host Application Insights settings. Requests are excluded from sampling. | There is no Azure Monitor or OpenTelemetry exporter in requirements. Confirm structured properties and dependencies. | + +These are source defaults, not guarantees. Runtime versions, packages, settings, filters, and +sampling can change the collected data. -Application Insights generally stores server spans as **requests**, client spans as -**dependencies**, and log messages as **traces**. +Application events are logs, not guaranteed `customEvents`. Verify the +[runtime format](MONITORING.md#runtime-log-formats) before saving queries. -### What the checked-in sample configures +For additional worker instrumentation, follow the +[Azure Functions OpenTelemetry guidance](https://learn.microsoft.com/azure/azure-functions/opentelemetry-howto) +for the selected language. Check correlation and duplicate records after any change. -The [deployment template](../setup/infra/resources.bicep) configures Application Insights, -authenticated ingestion, and a separate `allLogs`/`AllMetrics` diagnostic setting. Diagnostic -settings do not instrument every worker or dependency. + +## Signals and tables -| Implementation | Checked-in behavior | What not to assume | +| Signal | Workspace table | Notes | | --- | --- | --- | -| [.NET isolated](../dotnet/host.json) | The host selects `telemetryMode: OpenTelemetry`. The [project](../dotnet/dotnet.csproj) references the Functions worker OpenTelemetry integration, hosting extensions, and Azure Monitor exporter. | [Program.cs](../dotnet/Program.cs) does not explicitly register `AddOpenTelemetry().UseFunctionsWorkerDefaults().UseAzureMonitorExporter()`. Package references alone do not activate that direct worker export pipeline. Do not claim complete worker logs, dependencies, or exceptions from the host setting. | -| [JavaScript](../javascript/host.json) | Uses standard host Application Insights settings with sampling enabled and `Request` excluded from sampling. | The [package manifest](../javascript/package.json) does not include a direct Application Insights/OpenTelemetry exporter. Host integration is not proof that all outbound Node.js HTTP calls are traced. | -| [Python](../python/host.json) | Uses standard host Application Insights settings with sampling enabled and `Request` excluded from sampling. | The [requirements](../python/requirements.txt) do not include an Azure Monitor/OpenTelemetry exporter. A Python log message or `requests` call is not automatically a structured custom event or a dependency span. | +| HTTP requests | `AppRequests` | Includes duration, result code, success, role, and operation. Easy Auth can reject before invocation. HTTP success does not prove delivery. | +| Application logs | `AppTraces` | Includes messages, severity, and exported properties. Filters and sampling can remove records. | +| Outbound calls | `AppDependencies` | Includes only instrumented dependencies. Coverage differs by runtime and library. | +| Exceptions | `AppExceptions` | Includes captured and exported exceptions. Handled failures may appear only as sanitized logs or HTTP responses. | +| Custom metrics | `AppMetrics` | Requires emitted measurements and a working export path. | +| Custom events | `AppEvents` | Requires explicit event telemetry. A named log does not automatically become an event. | +| Availability results | `AppAvailabilityResults` | Requires an availability test or custom telemetry. Guided setup does not create tests. | -These are source defaults. Packages, settings, runtime versions, and filters can change them. +Application Insights metrics, workspace logs, Azure resource metrics, resource logs, Activity Log, +and Resource Health are different sources. A chart and a log query can use different sampling, +dimensions, and retention. Check the resource's supported metrics and diagnostic categories before +building an alert. -[Application events](LOGGING.md#event-sequence) are **logs**, not guaranteed `customEvents`. -Use [runtime-specific discovery](MONITORING.md#runtime-specific-log-discovery) before sharing queries: -exporters can change formatting, casing, and which properties survive. +See the [Application Insights data model](https://learn.microsoft.com/azure/azure-monitor/app/data-model-complete) +and [Functions monitoring reference](https://learn.microsoft.com/azure/azure-functions/monitor-functions-reference). -For direct worker instrumentation, follow the -[Functions OpenTelemetry guidance](https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto) -for the selected language and verify correlation and duplicate suppression. + +## Single region - -## Telemetry signals and tables +Guided setup connects one Function App to one Application Insights resource backed by a Log +Analytics workspace. -The following table maps **Application Insights Logs** names to **Log Analytics workspace** -names. Table availability and contents depend on what was actually emitted and ingested. +Check the following: -| Signal | Application Insights / workspace table | Interpretation and limits | -| --- | --- | --- | -| Invocations and HTTP request outcomes | `requests` / `AppRequests` | Name, duration, result code, success, role, and operation context. Requests rejected before the Function executes may not appear. A successful invocation does not prove delivery of an SMS or call. | -| Application and runtime logs | `traces` / `AppTraces` | Messages, severity, and available properties. Log filters or sampling can remove them; error-level text is not necessarily exception telemetry. | -| Outbound dependencies | `dependencies` / `AppDependencies` | Duration, target, type, and outcome for instrumented calls. HTTP, Key Vault, and credential calls are not guaranteed to be covered by every worker/runtime/library combination. Cached operations may make no remote call. | -| Recorded exceptions | `exceptions` / `AppExceptions` | Exceptions captured and exported by the active pipeline. Caught errors converted into sanitized logs or HTTP responses need not generate exception rows. | -| Emitted numeric measurements | `customMetrics` / `AppMetrics` | Custom instrumentation and, where supported, host aggregation measurements. Do not assume the classic host aggregator or custom metrics exist in every OpenTelemetry configuration. | -| Explicit custom events | `customEvents` / `AppEvents` | Requires an event-emitting instrumentation path. Giving a log an event name does not by itself populate this table. | -| Synthetic availability results | `availabilityResults` / `AppAvailabilityResults` | Requires configured tests or custom availability telemetry. Deploying Application Insights does not create these checks. | -| Performance counters | `performanceCounters` / `AppPerformanceCounters` | Depends on OS, hosting model, SDK, and enabled collection. Not a guaranteed source of Function CPU or memory data. | - -**Application Insights metrics** include request rates, failures, and durations derived from -application telemetry. Standard preaggregated metrics and log-based calculations can have -different sampling behavior and dimensions; a chart and a raw row count need not match. - -**Azure resource metrics** come from the platform and are queried under the relevant resource -in Azure Monitor Metrics. Function execution/instance/memory metrics depend on the hosting plan; -FC1 and EP1 do not expose identical sets. Key Vault has its own request/latency signals, Storage -has transaction/availability/latency signals, and Front Door adds edge/origin signals. -These are not automatically Application Insights dependencies or custom metrics. - -Resource logs are another independent source. Depending on the resource and diagnostic setting's -destination mode, exported logs use resource-specific tables or `AzureDiagnostics`; supported -exported metrics can appear in `AzureMetrics`. Not every platform metric or dimension is -exportable through diagnostic settings. Check the resource's supported categories and the -workspace schema rather than assuming a table exists. Azure Activity Log and Resource Health -provide additional control-plane and platform-health context, not application delivery outcomes. - -See the [Application Insights data model](https://learn.microsoft.com/en-us/azure/azure-monitor/app/data-model-complete) -and [Functions monitoring reference](https://learn.microsoft.com/en-us/azure/azure-functions/monitor-functions-reference) -for table definitions and plan-specific metrics. - -## Single-region configuration - -For one regional deployment, keep the relationship explicit: **one Function App sends application -telemetry to its selected Application Insights resource, backed by a Log Analytics workspace**. -The guided template creates that relationship. Separate production and test telemetry, and select -resource/workspace locations according to residency, access, retention, and cost requirements. - -Review the following in the portal without assuming that resource creation proves ingestion: - -1. On the Function App, identify the deployed language/package and the effective - `APPLICATIONINSIGHTS_CONNECTION_STRING` destination. Prefer the connection string over the - legacy instrumentation-key setting. Do not paste its value into issues or shared screenshots. -2. On Application Insights, verify the linked workspace and authenticated-ingestion configuration. - Verify the Function identity's scoped publisher role, not just the deployer's permissions. -3. Check the appropriate host and worker configuration described above, including effective - log levels and sampling. Independent worker exporters require their own configuration. -4. Use an authorized, non-delivering evaluation request from the supported onboarding procedure, - then inspect its request/log telemetry after allowing for ingestion delay. A `401` or `403` - is not a successful evaluation and may have been rejected before invocation. -5. Separately validate provider acceptance and actual recipient delivery through an approved, - controlled live test. Evaluation skips the provider send; it cannot validate that dependency - path. Background credential refresh may run independently during either test. - -Keep Easy Auth and the approved delivery flow intact while validating telemetry. Use an authorized -evaluation request for the authenticated, encrypted EPP path rather than generating periodic OTPs -or relying on a generic unauthenticated URL probe. - -### Identity and ingestion - -The current template sets `APPLICATIONINSIGHTS_AUTHENTICATION_STRING` to `Authorization=AAD`, -uses the Function App's **system-assigned managed identity**, assigns **Monitoring Metrics -Publisher** at the Application Insights resource, and sets `DisableLocalAuth: true`. -Despite its name, that role authorizes publishing application telemetry, not only metrics. -The connection string still selects the destination; it is not an Entra access token. - -The user-assigned identity used for outbound provider authentication is a separate concern. -If an operator deliberately selects a user-assigned identity for ingestion, the documented host -setting is `ClientId=;Authorization=AAD`; that identity must be attached to -the app and authorized at the destination. Do not substitute the provider application ID. - -Direct worker exporters must use a supported credential configuration. Do not assume that a -host authentication setting configures a manually added SDK. Local development also cannot use -an Azure-hosted managed identity by merely copying the settings. Follow the SDK's documented -local credential support against a nonproduction destination; do not enable local/key-based -ingestion on production to make a local test work. - -Missing data can result from a wrong destination, unsupported/uninitialized exporter, missing -role assignment, role propagation delay, identity mismatch, or blocked DNS/network access to -ingestion endpoints. Reader/query permissions are separate from ingestion permissions. -See [Functions collection configuration](https://learn.microsoft.com/en-us/azure/azure-functions/configure-monitoring) -and [Entra-authenticated ingestion](https://learn.microsoft.com/en-us/azure/azure-monitor/app/azure-ad-authentication). - -## Multi-region configuration - -Each region has its own Function App and identity. Configure and validate **every regional -origin independently**, even if traffic normally enters through one public URL. Application -Insights does not provide routing or failover. - -Choose the telemetry layout explicitly; adding a second Function region does not automatically -replicate or reconfigure an existing telemetry destination: - -| Layout | Configuration and tradeoff | -| --- | --- | -| Application Insights and workspace per region | Repeating the single-region setup produces distinct destinations. Point each Function to its regional component and authorize its identity there. Queries must include each workspace; retention/access are managed separately. Application failover does not guarantee regional telemetry remains queryable. | -| Application Insights per region, shared workspace | Each Function still targets its own component; components link to one approved workspace. Workspace queries can group by component resource ID. Retention, access, residency, and ingestion availability now share that workspace's boundary. This is an operator-selected design, not an automatic setup-script option. | -| Shared Application Insights component | Both Functions target the same component, with both identities granted ingestion access. Configure distinct, stable role names consistently in host and worker telemetry and verify them. Without a reliable role/region dimension, records cannot be separated by origin. Shared ingestion and quotas are also shared dependencies. | +1. **Destination:** On the Function App, confirm `APPLICATIONINSIGHTS_CONNECTION_STRING` points + to the intended resource. Do not copy it into issues or screenshots. +2. **Workspace:** On Application Insights, confirm the linked workspace. +3. **Ingestion authentication:** Confirm authenticated ingestion and the Function identity's + scoped publisher role. +4. **Runtime settings:** Review the selected runtime's host and worker configuration, log levels, + and sampling. +5. **Authorized request:** Run a non-delivering evaluation through the supported test process, + allow for ingestion delay, then check `AppRequests` and `AppTraces`. +6. **Live path:** Validate provider acceptance and recipient delivery separately through an + approved controlled test. + +A `401` or `403` can be rejected before the Function runs. Evaluation does not contact the provider. +Background credential refresh can run independently. + + +### Ingestion identity -Maintain an inventory mapping Function App, deployment region, Application Insights component, -workspace, and role name. A workspace's location or a telemetry item's client geography is **not** -proof of the Function's execution region. `AppRoleInstance` identifies an instance, not a stable -regional label. In a shared component, use verified role naming or an explicitly emitted region -property; do not expect Azure resource tags to become telemetry properties automatically. +The template configures: -Use the [cross-workspace queries and missing-origin check](MONITORING.md#3-multiple-regions-and-cross-workspace-queries) -with explicit sources, authorized access to each, and per-origin grouping. Check idle/standby -origins too: no traffic and broken collection can both look empty. +- `APPLICATIONINSIGHTS_AUTHENTICATION_STRING=Authorization=AAD` +- the Function App's system-assigned managed identity +- **Monitoring Metrics Publisher** on the Application Insights resource +- `DisableLocalAuth: true` -For the [Front Door multi-region design](FRONTDOOR.md), inspect -[edge metrics and logs separately](MONITORING.md#8-optional-azure-front-door-layer). -Edge/WAF rejections, origin-connectivity failures, and probes need not appear as Function requests. -Origin health does not prove provider authentication or delivery. +The connection string selects the destination. The managed identity authenticates ingestion. +The outbound provider identity is separate. + +If you intentionally use a user-assigned ingestion identity, set +`ClientId=;Authorization=AAD`, attach that identity to the app, and grant it +access to the destination. Do not use the provider application ID. + +Missing data can result from a wrong destination, missing role assignment, identity mismatch, +role propagation, an inactive exporter, filters, sampling, DNS, or network access. Query access is +separate from ingestion access. + +See [Functions collection configuration](https://learn.microsoft.com/azure/azure-functions/configure-monitoring) +and [Microsoft Entra-authenticated ingestion](https://learn.microsoft.com/azure/azure-monitor/app/azure-ad-authentication). + + +## Multiple regions + +Each region has its own Function App and identity. Validate telemetry for every origin. +Application Insights does not provide application routing or failover. + +| Layout | Use | +| --- | --- | +| Application Insights and workspace per region | Strong regional separation. Queries and access must include each workspace. | +| Application Insights per region with a shared workspace | Separate components with one query destination. Retention, access, and ingestion share the workspace boundary. | +| One shared Application Insights component | Requires distinct stable role names and ingestion access for every identity. Shared quotas and ingestion become common dependencies. | + +Keep an inventory of Function App, region, Application Insights component, workspace, role name, +runtime, and owner. Workspace location and client geography do not prove execution region. + +Use explicit sources and regional labels in +[multi-region queries](MONITORING.md#multiple-regions). Monitor Front Door edge and origin signals +separately from Function telemetry. -## Telemetry coverage queries +## Coverage checks -Run these examples from the **linked Log Analytics workspace's Logs** view. They use workspace -table/column names (`TimeGenerated`, `AppRoleName`, `OperationId`, `Properties`), not the -Application Insights aliases (`timestamp`, `cloud_RoleName`, `operation_Id`, `customDimensions`). -Choose the correct resource scope and time window. These examples are not alert definitions. +Run these in the linked Log Analytics workspace. -Start with request outcomes, keeping application components and roles separate: +### Requests ```kusto AppRequests @@ -189,12 +151,7 @@ AppRequests | order by Latest desc ``` -`ItemCount` is the sampling weight when supplied by the pipeline. The weighted sum estimates -represented requests; it does not restore discarded records or prove complete ingestion. If -weights are absent, investigate the exporter rather than treating an empty sum as zero traffic. -Do not add classic and OpenTelemetry duplicates or multiple diagnostic copies into one count. - -Discover which dependency types have actually been collected: +### Dependencies ```kusto AppDependencies @@ -204,7 +161,7 @@ AppDependencies | order by Latest desc ``` -Inspect log coverage by role and severity without returning message bodies or custom properties: +### Logs ```kusto AppTraces @@ -214,36 +171,24 @@ AppTraces | order by Latest desc ``` -An absent table means this query cannot run in that scope; it does not mean a healthy zero. -Empty results require checking time range, workspace/component scope, traffic, filters, -sampling, and ingestion. These fresh examples have not been executed against a live workspace -as part of this documentation change. Inspect request `OperationId` locally in the portal when -correlating an authorized test with its related logs/dependencies; request IDs, invocation IDs, -and application correlation IDs are not interchangeable. - -## Sampling, privacy, and collection limits - -- **Sampling is pipeline-specific.** JavaScript and Python exclude requests from their host - sampling configuration, not exceptions or every other signal. This does not protect against - log filtering, export failures, ingestion caps, or retention expiry. .NET's OpenTelemetry - host mode must not be described as using the same classic sampling settings. Align host and - worker sampling deliberately and validate correlation after any instrumentation change. -- **Log filters affect visibility.** In the classic host pipeline, raising `Host.Results` above - `Information` can remove successful invocation records; worker filters are independent when - logs bypass the host. An empty Failures view can reflect missing collection, not healthy code. -- **Telemetry is not an exactly-once record.** Buffering, batching, cold starts, process - termination, network failures, throttling, daily caps, and retention affect what is available. - Ingestion has latency; Live Metrics is not a durable substitute for stored telemetry. -- **Keep sensitive content out of every pipeline.** Do not log phone numbers, OTPs, message - bodies, encrypted request bodies, evaluation nonces, authorization headers, tokens, API keys, - private keys, or provider response bodies. Review automatically captured URLs, query strings, - dependency attributes, and exception text as well as application logs. Sanitized application - logging does not sanitize a newly added auto-instrumentation library. -- **Control access and cost.** Restrict query/export access, set an appropriate retention policy, - and review ingestion volume before enabling verbose logs or additional instrumentation. - Separate environment data; use aggregates rather than customer records in shared reports. -- **Know the boundary.** Host success, HTTP success, provider acceptance, and recipient delivery - are different outcomes. Handled failures need not be exceptions; credential refresh can - occur without an invocation; an idle or scaled-to-zero app may emit nothing. Platform health, - external provider status, and independently designed availability tests supply different - evidence. None is created merely by connecting Application Insights. +An absent table, query error, or empty result is not a healthy zero. Check scope, time range, +traffic, access, filters, sampling, and ingestion. `ItemCount` is a sampling weight when supplied. +It does not recover discarded records. + + +## Limits + +- **Sampling and filters:** JavaScript and Python exclude host requests from sampling, not every + signal. .NET OpenTelemetry mode does not use the same classic settings. +- **Delivery:** Host success, HTTP success, provider acceptance, and recipient delivery are + different outcomes. +- **Completeness:** Buffering, cold starts, process exits, network failures, throttling, caps, + retention, and exporter configuration affect stored telemetry. +- **Privacy:** Do not collect phone numbers, OTPs, request bodies, encrypted payloads, nonces, + tokens, credentials, private keys, or raw provider responses. Review auto-captured URLs, + query strings, dependency fields, and exception text. +- **Access and cost:** Restrict query and export access. Set retention and daily caps deliberately. + A cap can create a monitoring blind spot. + +Next: use the [monitoring tasks and queries](MONITORING.md) or the +[endpoint troubleshooting guide](../setup/docs/Troubleshooting.md#after-deployment-triage). diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index a1e38f7..abc042a 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -1,12 +1,18 @@ # Estimate SMS provider costs -Estimate SMS and voice traffic before onboarding an External Phone Provider (EPP). The report -groups recent authentication steps by calling code, such as `+1` or `+91`. Apply provider rates -to estimate a budget. +Estimate SMS and voice traffic before onboarding a provider. The report groups recent +authentication steps by calling code. Apply provider rates to estimate a budget. -**This is not a billing report.** It counts each matching authentication step as one estimated -request. Confirm retries, fallback, segmentation, destinations, taxes, fees, and pricing with -the provider. +> [!IMPORTANT] +> This is not a billing report. Confirm retries, fallback, message segments, destinations, taxes, +> fees, and pricing with the provider. + +| Step | Task | +| --- | --- | +| 1 | [Prepare access](#prerequisites) | +| 2 | [Generate the report](#generate-the-activity-report) | +| 3 | [Calculate the estimate](#convert-activity-into-a-cost-estimate) | +| 4 | [Confirm provider rates](#confirm-provider-rates) | The `authenticationDetails` data used by this guide is currently available through the [Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta). @@ -33,10 +39,8 @@ approved folder. ## Generate the activity report -The sample installs the required Microsoft Graph modules and connects to Microsoft Graph. -It finds SMS and voice authentication steps, matches each masked phone detail to a registered -phone method, and caches each user's phone-method lookup. Set `$start` and `$end` to the -required reporting period. +The script installs the required modules, finds SMS and voice authentication steps, matches masked +details to registered phone methods, and groups the result. Set `$start` and `$end` first. ```powershell # 1. Install required modules if they are missing @@ -151,27 +155,16 @@ $report | Format-Table -AutoSize $report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTypeInformation ``` -Microsoft documents `SMS` and `Voice` as authentication method values and states that -`authenticationMethodDetail` can contain the phone number for those methods. Some tenant -sign-in results report SMS as `Text message`; the script accepts both values and normalizes -them to `SMS` in the exported report. It does not use the cmdlet's `-Property` parameter, -because `authenticationDetails` is returned on the beta sign-in objects without that -selection and the parameter has not behaved consistently across module versions. - -The phone detail can be masked, for example `+x XXXXXXXX90`. The script removes non-digits -from that detail and matches the visible suffix (`90`) against the user's registered -methods. It then extracts the calling code from the uniquely matched registered number, -whose Graph format includes a space after the code, such as `+91 1234567890`. - -For SMS, the script uses `mobile` and `alternateMobile` registrations. Voice can use any -registered phone type. The report uses `Unknown` when the visible detail is too short, -does not match one registered number, or has an unexpected format. This avoids guessing -a destination. Review the `Unknown` total before relying on the estimate. - -The script does not use sign-in geography. `-All` requests all available pages, but large -tenants should still account for Microsoft Graph throttling and execution time. The script -uses `-ErrorAction Stop` rather than silently producing a partial report when either -sign-in or phone-method retrieval fails. +The script accepts `SMS`, `Text message`, and `Voice`, then normalizes text messages to `SMS`. + +For a masked value such as `+x XXXXXXXX90`, the script matches the visible suffix to one registered +method and extracts its calling code. + +SMS uses `mobile` and `alternateMobile` registrations. Voice can use any phone type. The report +uses `Unknown` rather than guessing when it cannot find one match. Review that total. + +The script does not use sign-in geography. Large tenants should account for Microsoft Graph +throttling and execution time. Errors stop the report instead of returning partial results. The Microsoft Graph [list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) diff --git a/docs/FRONTDOOR.md b/docs/FRONTDOOR.md index c9cd1aa..e5446ed 100644 --- a/docs/FRONTDOOR.md +++ b/docs/FRONTDOOR.md @@ -5,43 +5,39 @@ [Documentation](README.md) | [Single-region setup](GETTING-STARTED.md) | [Routing troubleshooting](../setup/docs/Troubleshooting.md#optional-front-door-deployments) -Azure Front Door provides **one public EPP URL across regional Function Apps**. Start with a -validated [single-region deployment](GETTING-STARTED.md), then add origins, readiness, routing, -validation, failover drills, and rollback. +Azure Front Door provides one public EPP URL across regional Function Apps. Start with a validated +[single-region deployment](GETTING-STARTED.md). -Follow [scope](#scope-and-observed-behavior), [architecture](#architecture), -[plan/capacity](#1-plan-the-regional-deployment), [origins/keys](#2-prepare-equivalent-independently-provisioned-origins), -[readiness](#3-provide-a-non-delivering-readiness-endpoint), [routing](#4-configure-front-door-manually), -[validation](#5-validate-before-manually-activating-policy), and [fire-drill](#6-regional-failover-fire-drill). -Use [historical results](#observed-failover-results-and-limitations) as test context. +| Step | Task | +| --- | --- | +| 1 | [Plan regions and capacity](#1-plan-the-regional-deployment) | +| 2 | [Prepare equivalent origins and keys](#2-prepare-regional-origins) | +| 3 | [Add a non-delivering readiness endpoint](#3-provide-a-non-delivering-readiness-endpoint) | +| 4 | [Configure Front Door](#4-configure-front-door-manually) | +| 5 | [Validate the complete route](#5-validate-before-manually-activating-policy) | +| 6 | [Run a failover drill](#6-regional-failover-fire-drill) | -## Scope and observed behavior - -Testing used JavaScript origins, Front Door Standard, two or three regions, a shared key, and a -non-delivering readiness handler. +[Historical test results](#observed-failover-results-and-limitations) are context, not an SLA. -Add the [readiness contract](#3-provide-a-non-delivering-readiness-endpoint) to each regional -package before enabling probes. Released packages remain single-region. +## Scope and observed behavior -Build acceptance tests around the selected runtime and features, including approved live checks, -Microsoft caller behavior, dependency loss, networking, and realistic load. +The recorded tests used JavaScript, Front Door Standard, two or three regions, one shared +encryption key, and a non-delivering readiness handler. Released packages remain single-region, +so add and validate the readiness route before enabling probes. ### Regional resiliency responsibility -Customers own capacity, credentials, keys, monitoring, and approved drills. Validate any -alternate topology separately. - -Set RTO/RPO, failure, latency, capacity, and evidence targets for the regional design. +Customers own capacity, credentials, keys, monitoring, drills, and rollback. Set recovery, +latency, capacity, failure, and evidence targets before testing. ## Architecture ![Multi-region External Phone Provider architecture with Azure Front Door](images/multi-region-architecture.png) -Region names are examples. Give each Function App, storage account, and Key Vault a unique name. -Keep **provider and authentication configuration consistent and use the same RSA key**. -Evaluation returns the nonce without contacting the provider. API-key integrations use secrets; -OAuth uses each region's outbound identity. Match key material, not vault URLs or version IDs. +Give each regional Function App, storage account, and Key Vault a unique name. Keep provider and +authentication settings consistent and use the same RSA key. Match key material, not vault names +or version IDs. | Component | Purpose | |---|---| @@ -55,33 +51,26 @@ priority, latency, and weight. Equal weights do not guarantee a 50/50 split. ### Two-region baseline to provision and validate -Use two regional stacks at equal priority. Each has its own Function App, capacity, storage, -Key Vault, identities, telemetry, matching decryption key, and provider credentials. Verify each -can operate without the other region. - -Share the public URL, endpoint app, issuer, audience, caller policy, provider route, and public-key -identity. Replicate the private key using [key coordination](#coordinate-the-encryption-key). -Front Door, Entra authentication, and the provider remain shared dependencies. +Start with two equal-priority regional stacks. Each region has its own Function App, capacity, +storage, Key Vault, identities, telemetry, matching key, and provider credentials. Verify each can +operate without the other. Front Door, Microsoft Entra authentication, and the provider remain +shared dependencies. ## 1. Plan the regional deployment -Build the regional design in an isolated nonproduction tenant and subscription, separate from -live traffic. Review the +Build and test the regional design outside live traffic. Review the [setup prerequisites](../setup/docs/README.md#prerequisites-for-step-2) and [regional quota guidance](../setup/docs/Troubleshooting.md#deployment-fails-with-subscriptionisoverquotaforsku). -- Start with two regions, based on service availability, capacity, residency, and the selected - Security Store provider's requirements. A third region adds cost and capacity, but does not - necessarily shorten failure detection. +- Choose regions based on service availability, capacity, residency, and provider requirements. - Check availability of **every** required resource type, not just EP1 quota. Hosting availability does not mean Application Insights or other dependencies are available there. - Budget for an EP1 plan and regional dependencies in each region, Front Door, and telemetry. - Size and validate each region for the survivor workload described below, not just its normal share. -- Choose one language and verify that each origin receives the same package. Do not mix - implementations or package versions while measuring failover. + Size each region for the survivor workload, not only its normal share. +- Use one runtime and the same package in every region. - Record the endpoint application's client ID, validated token audience and issuer, authorized caller application ID, certificate/public-key identity, selected provider route, and regional owners. -- Plan manual rollout, validation, certificate renewal, monitoring, and rollback before activation. +- Plan rollout, validation, renewal, monitoring, drills, and rollback before activation. Use the regional procedure in this guide rather than rerunning `Setup-Epp.ps1` against the same application. The setup script creates one region; the steps below coordinate Front Door, shared @@ -91,17 +80,12 @@ application configuration or certificate unexpectedly. ### Regional capacity -**Size and validate each region to serve the full combined peak workload.** In a balanced -two-region active-active design, each region needs capacity for **at least twice its normal -traffic**. Add room for bursts, approved retries, probes, and dependencies. Reserve this -capacity in both A and B before using regional failover. Retry capacity does not change the rule -to check an uncertain live send before retrying. - -Use the expected peak workload rather than a quiet-period average. Equal weights do not guarantee -equal routing. If traffic is uneven, each region still needs the combined peak plus headroom, so -the less-used region may need more than twice its usual traffic. For more than two regions, -document the failure budget and measure capacity for every allowed survivor group and routing -pattern. +Size and validate each region for the full combined peak workload. In a balanced two-region +design, each region needs at least twice its normal traffic plus room for bursts, probes, and +dependencies. + +Use peak workload, not a quiet-period average. Equal weights do not guarantee equal routing. +For more than two regions, test every supported survivor combination. Record the workload mix, offered and completed request rates, concurrency, payload sizes, test duration, per-region latency percentiles, errors/timeouts, resource saturation, and dependency diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 9c07668..f9901ae 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -4,30 +4,31 @@ [Documentation](README.md) | [Setup reference](../setup/docs/README.md) | [Troubleshooting](../setup/docs/Troubleshooting.md) -Deploy **one regional endpoint** for Microsoft Entra ID SMS and voice requests. Guided setup -publishes the selected package. Validate each channel you plan to use. +Deploy one regional endpoint for Microsoft Entra ID SMS and voice requests. -Follow in order: [prepare](#1-get-ready), [register](#2-register-your-application), -[deploy](#3-run-guided-setup), [connect and test](#4-connect-your-provider-and-test), -then [activate and hand over](#5-activate-and-hand-over). -Portal labels may differ from the screenshots. +| Step | Task | +| --- | --- | +| 1 | [Prepare access, tools, region, and budget](#1-get-ready) | +| 2 | [Register the endpoint application](#2-register-your-application) | +| 3 | [Run guided setup](#3-run-guided-setup) | +| 4 | [Connect the provider and test](#4-connect-your-provider-and-test) | +| 5 | [Activate and hand over](#5-activate-and-hand-over) | + +Complete the steps in order. Portal labels can differ from the screenshots. ## 1. Get ready -Start with a **separate test environment** in a nonproduction tenant and subscription. Create a -new application registration and choose a unique resource prefix. This makes the endpoint easy -to identify and keeps testing separate from production. +Use a separate test environment with its own application registration and resource prefix. -Before purchasing or deploying, confirm access to a supported provider's EPP integration through +Before purchasing or deploying, confirm provider EPP access in [Microsoft Security Store](https://securitystore.microsoft.com/private-solutions) and arrange -Microsoft's approved testing and activation procedure. See +Microsoft's approved test and activation process. See [access and support](ONBOARDING.md#before-purchasing-or-deploying). In Security Store, open **Private solutions** and select the approved subscription. Check [guided provider support](ONBOARDING.md#provider-credential-names). -Use the [SMS cost estimation guide](COST-CALCULATOR.md) to create a planning estimate, -then confirm destination rates, fees, and final costs with your provider. +Use the [cost estimate](COST-CALCULATOR.md), then confirm rates and fees with the provider. Prepare: @@ -41,8 +42,8 @@ Prepare: - **Azure region and budget:** check availability and subscription quota for your chosen Linux plan. Azure resources and provider services can incur charges. -Check the [full prerequisites](../setup/docs/README.md#prerequisites-for-step-2), including the -Microsoft Entra ID allowed-tenants preview. Setup can install Bicep, but not Azure CLI. +Check the [full prerequisites](../setup/docs/README.md#prerequisites-for-step-2). Setup can install +Bicep, but not Azure CLI. Assign an **onboarding owner** and review [production readiness](../TECHNICAL.md#production-readiness). @@ -91,15 +92,15 @@ Review the downloaded script, then run it: .\Setup-Epp.ps1 ``` -On a shared workstation, use `.\Setup-Epp.ps1 -ForceAuthentication` for a fresh Azure CLI sign-in. +On a shared workstation, use `.\Setup-Epp.ps1 -ForceAuthentication` for a fresh sign-in. Select the tenant, subscription, application **client ID**, and region. Then choose: - **Provider and route:** enter or confirm the provider by name. **Tenant scope** means Global/EU provider routing, not Azure region or a data-residency guarantee. - **Platform:** JavaScript, C#, or Python. -- **Hosting:** FC1 can scale to zero and cold-start; EP1 has paid warm capacity. FC1's usage grant - does not make deployment free. Review [plan details](../setup/docs/README.md#service-plan-selection). +- **Hosting:** FC1 can scale to zero and cold-start. EP1 has paid warm capacity. Review + [plan details](../setup/docs/README.md#service-plan-selection). - **Resource prefix:** 2-8 lowercase letters/digits, starting with a letter. Complete required sign-in, MFA, or administrator consent. Resolve preflight errors before approval. @@ -108,17 +109,16 @@ For existing encryption credentials, follow [certificate selection](../setup/docs/README.md#encryption-certificate-selection). Coordinate changes across 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, and resource names. Also check -permissions, certificate key IDs, and access to any existing vault. The plan includes Microsoft Graph `Application.Read.All` for the Microsoft phone-provider service -principal. `No` or Enter cancels deployment. +**Review the deployment plan before typing `Yes`.** Check accounts, region, provider URLs, +runtime, plan, cache settings, resource names, permissions, certificate key IDs, and vault access. +The plan includes Microsoft Graph `Application.Read.All` for the Microsoft phone-provider service +principal. `No` or Enter cancels. **Plan review:** run interactively and choose **No** at the final prompt. See [plan review](../setup/docs/README.md#reviewing-the-plan-without-deploying). -Wait for **Deployment completed** and the saved summary before Step 4. Setup creates the -Function App, storage, Key Vault, identities, and monitoring as one coordinated deployment. -Begin without precreating these resources so setup can apply the complete reviewed plan. +Wait for **Deployment completed** and the saved summary before Step 4. Setup creates the Function +App, storage, Key Vault, identities, and monitoring. Do not precreate these resources. Keep `epp-output` private. Its [summary](../setup/docs/README.md#read-the-deployment-summary) identifies the resources, endpoint, app, and encryption key. No private key is downloaded. @@ -131,9 +131,7 @@ listed in your deployment summary. ![Example regional resource group showing a Function App, monitoring, Key Vault, managed identity, hosting plan, and storage](images/onboarding/resource-group.png) -*This existing Premium test deployment shows the resource types to look for, not a new -deployment or a guarantee that your resource list will be identical. Use setup rather than -the portal's Create button to deploy the endpoint.* +*This Premium test deployment shows the expected resource types. Your list can differ.* ## 4. Connect your provider and test @@ -149,8 +147,7 @@ In your Function App, open **Settings > Authentication**. Check that authenticat ![Function Authentication page showing Enabled, Require authentication, and Return HTTP 401 Unauthorized](images/onboarding/authentication.png) -*The **Microsoft** identity provider on this page validates the caller. It is not the phone -provider you selected for SMS or voice.* +*The **Microsoft** identity provider validates the caller. It is not the SMS or voice provider.* Complete the [approved endpoint checks](ONBOARDING.md#validate-the-deployed-endpoint): diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 3cec701..6ad0bf2 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -2,7 +2,14 @@ [Documentation](README.md) | [Find deployed logs](MONITORING.md#runtime-specific-log-discovery) | [HTTP contract](CONTRACT.md) -Use this reference with the [monitoring walkthrough](MONITORING.md). +Use this page to identify safe event fields and runtime differences. + +| Need | Section | +| --- | --- | +| Compare runtime output | [Runtime formats](#runtime-formats) | +| Follow one request | [Event sequence](#event-sequence) | +| Interpret IDs and fields | [Safe fields and identifiers](#safe-fields-and-identifiers) | +| Configure collection | [Levels and collection](#levels-and-collection) | **Exclude request bodies, decrypted fields, credentials, nonces, raw exceptions, and provider responses.** Keep body tracing off. @@ -15,8 +22,8 @@ responses.** Keep body tracing off. | Python | Standard `logging`; `otp_log.py` defines fixed event IDs, names, levels, and fields. An immutable `LoggerAdapter` supplies Function and Microsoft trace identifiers. | | .NET | Standard `ILogger`; `OtpLog` defines source-generated IDs/names. `ILogger.BeginScope` supplies `FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`, `MsCorrelationId`, and `MsCorrelationIdSource`. | -Each invocation ends with `request_completed`. Logger/exporter behavior controls formatting and -property preservation. JavaScript, Python, and .NET field names differ; verify the +Each invocation ends with `request_completed`. Exporters control formatting and property +preservation. Field names differ by runtime, so verify the [deployed schema](MONITORING.md#runtime-specific-log-discovery). ## Event sequence @@ -61,21 +68,17 @@ Unknown status text and malformed JSON are never logged; malformed JSON emits on ## Safe fields and identifiers -Build metadata from **validated allowlisted fields**. Exclude unknown, tenant, locale, risk, and -decrypted data. +Build metadata from validated allowlisted fields. Exclude unknown, tenant, locale, risk, and +decrypted values. `providerCredentialSource` is `managed_identity_client_assertion` for app tokens and `key_vault` for API keys. Cached resolution does not imply a network fetch or fresh token exchange. `providerCredentialElapsedMs` includes resolution and usability checks, not a separate deadline. SDK diagnostics remain suppressed. -`provider_request_built` describes the outbound request, not a new physical connection or HTTP -client. `providerEndpoint` records the **final** scheme, host/port, and path, which can differ -from the configured base URL. For example, `https://provider.example/epp/voice?key=secret` becomes -`https://provider.example/epp/voice`. Userinfo, query strings, and fragments are excluded, but -**path segments are not redacted**: use provider-approved paths without secrets/personal data. -Logging does not alter the actual URL or required query parameters. Standard verbs are uppercase; -custom/invalid verbs appear as `other` without changing the sent request. +`providerEndpoint` records the final scheme, host, port, and path. It removes user information, +query strings, and fragments, but not path segments. Keep secrets and personal data out of URL +paths. Logging does not change the request. JavaScript/Python events carry Function request/invocation IDs and Microsoft trace IDs available at that stage. JavaScript also carries known channel/evaluation/provider context; Python uses @@ -97,13 +100,10 @@ Microsoft IDs are absent or change. Log names deliberately distinguish these sou | `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. | -The six external/configuration ID fields preserve raw values and case, without hashing/truncation. -Only nonblank strings of **1-128 ASCII characters** are recorded: a letter/digit first, then -letters, digits, `.`, `_`, `:`, or `-`. Missing/blank values are null; other invalid values are -null and their names appear in `omittedIdFields`, never their values. -These guards affect logs only, not wire IDs, correlation precedence, or outcomes. -Application/client/tenant IDs are identifiers, not secrets or trust assertions; incoming -`tenantId` never becomes a trusted identity. +External IDs keep their original value and case. Logs accept only 1-128 ASCII characters, starting +with a letter or digit and followed by letters, digits, `.`, `_`, `:`, or `-`. Missing or invalid +values are null. Invalid field names appear in `omittedIdFields`, never their values. These checks +change logs only, not request behavior or trust. ## Levels and collection diff --git a/docs/MONITORING.md b/docs/MONITORING.md index f92ba83..47501a7 100644 --- a/docs/MONITORING.md +++ b/docs/MONITORING.md @@ -1,76 +1,52 @@ -# Monitoring +# Monitor the endpoint -[Documentation](README.md) | [Collection diagnostics](APPLICATION-INSIGHTS.md) | [Log schema](LOGGING.md) | [Endpoint troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage) +[Documentation](README.md) | [Application Insights](APPLICATION-INSIGHTS.md) | [Log fields](LOGGING.md) | [Troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage) -Guided setup creates Application Insights and Log Analytics. Use this guide to query them and -configure alerts. +Guided setup creates Application Insights and Log Analytics. Use this page to confirm telemetry, +review failures and latency, and create alerts. -For identity signals, use [Microsoft Entra ID sign-in monitoring](SIGN-IN-MONITORING.md). - -For a first single-region deployment: - -1. [Find the monitoring resources and confirm data is arriving](#1-record-the-deployment-and-confirm-telemetry) - after an [authorized test](ONBOARDING.md#validate-the-deployed-endpoint). -2. Check [requests, failures, and latency](#2-single-region-function-queries), then use - [the log format for your runtime](#runtime-specific-log-discovery). -3. [Set up and test alerts](#5-set-up-notifications-and-alert-rules), including certificate - reminders and cost ownership. - -For missing data, use [collection checks](#9-sampling-ingestion-gaps-privacy-and-validation). -See also [multiple regions](#3-multiple-regions-and-cross-workspace-queries), -[Workbook](#6-build-a-shared-operations-workbook), [availability checks](#7-availability-authenticated-functions-and-fc1), -and [Front Door](#8-optional-azure-front-door-layer). - -Review permissions, privacy, and charges. Tune sample thresholds to the deployment. - - -## 1. Record the deployment - -Keep a private inventory with one row per expected regional endpoint: - -| Inventory field | Purpose | +| Task | Start here | | --- | --- | -| Region and Function App resource ID | Identify the deployment and platform metric scope. | -| Application Insights resource ID and Log Analytics workspace ID | Identify the application telemetry source and query permissions. | -| Observed `AppRoleName` and `AppRequests.Name` | Filter this Function and its `SendOtp` request operation without mixing other apps or readiness probes. | -| Runtime, hosting plan, deployment version, and owner | Explain schema differences, cold starts, releases, and escalation. | -| Expected traffic or authorized synthetic-test cadence | Decide whether missing data is actionable, including standby origins. | -| Front Door profile, endpoint, origin group, and origin | Associate regional routing and health with the Function. | - -Build the inventory from the deployment record, not recent telemetry. - -Guided setup does not create action groups, Workbooks, or alerts. Verify -[collection](APPLICATION-INSIGHTS.md#single-region-configuration) and effective retention. +| Confirm data is arriving | [Open the resources](#open-the-monitoring-resources) | +| Review requests, failures, and latency | [Function requests](#function-requests) | +| Review evaluation and provider outcomes | [Application events](#application-events) | +| Find Python or .NET log fields | [Runtime log formats](#runtime-log-formats) | +| Monitor several regions | [Multiple regions](#multiple-regions) | +| Create notifications | [Alerts](#alerts) | +| Diagnose missing data | [Collection and data quality](#collection-and-data-quality) | +| Monitor Microsoft Entra sign-ins | [Sign-in monitoring](SIGN-IN-MONITORING.md) | + +Run queries only after an [authorized test](ONBOARDING.md#validate-the-deployed-endpoint). +Keep phone numbers, OTPs, request bodies, credentials, tokens, nonces, and raw provider responses +out of queries, alerts, exports, and screenshots. + -### Portal resources +## Open the monitoring resources -1. Open the Function App named in your deployment summary. -2. Select **Monitoring > Application Insights** and follow the linked resource name. - Viewing logs does not require **Change your resource** or **Apply**. +1. Open the Function App from the deployment summary. +2. Select **Monitoring > Application Insights**, then open the linked resource. -![Function App Monitoring menu with Application Insights selected and its connected resource link](images/onboarding/monitoring-link.png) + ![Function App Monitoring menu with Application Insights selected and its connected resource link](images/onboarding/monitoring-link.png) -3. On that Application Insights resource's **Overview**, find **Logs workspace** and open - the linked workspace. Then select **Logs** in the workspace to use the queries below. +3. On the Application Insights **Overview**, open **Logs workspace**. +4. In the workspace, select **Logs** and use **KQL mode**. -![Application Insights Overview showing the Logs workspace link with account and connection details redacted](images/onboarding/monitoring-workspace.png) + ![Application Insights Overview showing the Logs workspace link with account and connection details redacted](images/onboarding/monitoring-workspace.png) -Screenshots hide identifiers. Portal names and layout may differ. +Record the Function App, Application Insights resource, workspace, runtime, region, deployment +version, owner, and expected traffic or test schedule. For several regions, keep one row per origin. -Keep connection strings private. `AppRequests`/`AppTraces` are not interchangeable with -`FunctionAppLogs` or `AzureDiagnostics`. -See [pipelines and table names](APPLICATION-INSIGHTS.md#signals-tables-and-metrics-are-different-things). +Connection strings and resource identifiers are private operational data. Grant read access only +to the required workspace. Application telemetry uses `AppRequests`, `AppTraces`, and related +tables. Function platform logs and resource diagnostics can use other tables. See +[telemetry tables](APPLICATION-INSIGHTS.md#signals-tables-and-metrics-are-different-things). -Run the KQL below in **Log Analytics workspace > Logs** and replace every placeholder. -Application Insights uses a different table schema. `_ResourceId` identifies the Application -Insights component, not the Function App. +### Discover the deployed role and request name -Run this discovery query after a controlled authorized test. The examples use -`SendOtp` as the request name; replace that literal everywhere if your observed -`Name` differs. Confirm that the row really represents the OTP operation. +Run this after an authorized request: ```kusto AppRequests @@ -80,19 +56,11 @@ AppRequests | order by LastSeen desc ``` -Grant only required read access on **each workspace**. Workbook sharing does not grant data access; -alert evaluation needs its own identity's access, not the author's. +Use the observed `AppRoleName`, `_ResourceId`, and request `Name` in later queries. Do not assume +that every deployment records the operation as `SendOtp`. -### First query - -1. In your workspace's **Logs**, close any welcome or Queries hub dialog. If the portal opens - in **Agent** mode, switch it off for this walkthrough, then choose **KQL mode**. -2. Paste the query below. Replace `` with the `AppRoleName` you verified - using the discovery query above. If a workspace contains multiple Application Insights - components with that same role name, also filter by your component's `_ResourceId`. -3. Select **Run**, then read the **Results** grid. Adjust `ago(48h)` to include the time of - your authorized test; this query sets its own time range. +### First check ```kusto AppRequests @@ -104,22 +72,15 @@ AppRequests ![Workspace Logs in KQL mode with a scoped request-status query, Run button, and one aggregate result row](images/onboarding/logs-query-results.png) -*This real query returned 11 stored rows with result code 200 and `Success=true` in the example -window. The role and workspace names are hidden. The query shows counts only: no phone numbers, -message bodies, credentials, or raw trace messages. Your result counts will differ.* +`StoredRows` counts stored telemetry, not delivered messages. Sampling can change the count. +Evaluation and live calls can both return `200`. No rows means no matching data in this scope and +time range, not proof that the endpoint is healthy. -`StoredRows` counts stored telemetry records, not delivered messages or necessarily all requests. -Sampling can change the count, and evaluation and live calls can both return HTTP 200. No rows -means no matching records in the selected scope/time range; it is not proof that nothing failed. -Use the scoped and sampling-aware queries below for ongoing monitoring. - -## 2. Single-region Function queries + +## Function requests ### Requests, failures, and latency -Scope to both the component and role, not merely the operation name. This query -reports five-minute buckets from the last hour: - ```kusto AppRequests | where TimeGenerated >= ago(1h) @@ -137,12 +98,10 @@ AppRequests | order by TimeGenerated asc ``` -`ItemCount` weights sampled records; counts/percentiles remain estimates, not recovery of lost data. -Empty buckets mean **no data**, not zero failures or availability. `Success` is a request outcome, -not handset receipt. HTTP 200 includes both evaluation and live calls; use -[classified service events](#separate-evaluated-accepted-and-failed-completions) where verified. +`ItemCount` weights sampled records. Empty buckets mean no data. `Success` is an HTTP request +outcome, not confirmation that a recipient received an SMS or call. -### Failure status breakdown +### Failure status ```kusto AppRequests @@ -155,110 +114,15 @@ AppRequests | order by TimeGenerated asc ``` -Investigate application 4xx separately from provider/host 5xx. An Easy Auth -rejection can happen **before the Function executes** and need not produce an -`AppRequests` row or service event. Correlate with the available platform HTTP/auth -diagnostics and caller-side observations. Do not disable authentication to make a -monitor green. - -## 3. Multiple regions and cross-workspace queries - -For multiple apps in one workspace, query `AppRequests` once and keep -`_ResourceId` and `AppRoleName` as grouping keys. For separate regional workspaces, -use explicit workspace references. This two-region example keeps the regions -separate so a healthy region cannot hide another region's failures: - -```kusto -union - (workspace("").AppRequests - | where TimeGenerated >= ago(1h) - | where _ResourceId =~ "" - and AppRoleName == "" and Name == "SendOtp" - | extend Region = "region-a"), - (workspace("").AppRequests - | where TimeGenerated >= ago(1h) - | where _ResourceId =~ "" - and AppRoleName == "" and Name == "SendOtp" - | extend Region = "region-b") -| extend Weight = tolong(coalesce(ItemCount, 1)) -| summarize Requests = sum(Weight), - Failed = sumif(Weight, Success == false), - UnknownOutcome = sumif(Weight, isnull(Success)), - P95Ms = percentilew(DurationMs, Weight, 95), - P99Ms = percentilew(DurationMs, Weight, 99) - by Region, _ResourceId, AppRoleName, bin(TimeGenerated, 5m) -| extend FailurePct = 100.0 * Failed / Requests -| order by TimeGenerated asc, Region asc -``` - -Use distinct sources and filters to avoid double counting. If combining an -overall failure rate, divide total failures by total requests; do not average -regional percentages. Query raw rows for an overall percentile rather than -averaging regional p95 values. A regional origin's request share depends on -routing policy, traffic geography, failover, and probe traffic; equal shares are -not an availability requirement. +Investigate application `4xx` separately from provider or host `5xx`. Easy Auth can reject a +request before the Function runs, so some `401` or `403` responses may not create an +`AppRequests` row or application event. -Keep workspace references explicit in alert queries: they cannot be runtime -parameters. Do not use `isfuzzy=true` or best-effort unions to conceal an inaccessible -workspace. A query error is a monitoring failure to fix, not evidence of health. -See [cross-workspace query requirements][cross-workspace]. + +## Application events -### Missing telemetry against the expected inventory - -Use this only when each listed origin is **expected to receive matching requests** -in the observation window. For one region, keep one inventory row and use local -`AppRequests` as the `Observed` source. For multiple apps sharing a workspace, also -use the local table once instead of unioning the same table twice. - -The example deliberately evaluates a delayed window, from 20 minutes ago to -5 minutes ago, to allow ingestion time. Replace both the inventory and workspace -placeholders. The component IDs are normalized to lowercase for the join. - -```kusto -let Expected = datatable(Region:string, Component:string, Role:string) -[ - "region-a", "", "", - "region-b", "", "" -] -| extend Component = tolower(Component); -let Observed = - union workspace("").AppRequests, - workspace("").AppRequests - | where TimeGenerated between (ago(20m) .. ago(5m)) - | where Name == "SendOtp" - | summarize StoredRows = count(), LastSeen = max(TimeGenerated) - by Component = tolower(_ResourceId), Role = AppRoleName; -Expected -| join kind=leftouter Observed on Component, Role -| extend StoredRows = coalesce(StoredRows, tolong(0)) -| extend MissingTelemetry = StoredRows == 0 -| project Region, Component, Role, StoredRows, LastSeen, MissingTelemetry -``` - -Because the inventory is the left side, an entirely empty `Observed` result -still produces one missing row for **every** expected origin. An ungrouped -`count()` alone or an inventory built from recent rows cannot provide that check. -A missing/inaccessible table still fails the query; it must not be treated as an -empty but valid source. - -For an alert, append `| where MissingTelemetry` and trigger on **table rows > 0**. -Use a 20-minute query lookback so the query can inspect its complete delayed -window. Split by `Region` (and component/role if needed). Absence of real user -traffic is not an outage: an idle FC1 app, passive standby, or origin no longer -receiving traffic after failover can legitimately report nothing. Monitor an -authorized scheduled test's own result and schedule separately if a heartbeat is -required; the request query is not a synthetic scheduler. - -## 4. JavaScript service-event queries - -These queries require JavaScript invocation logs containing `logType: "service"` and `eventName` -as JSON in `AppTraces.Message`. **Do not apply this parser to Python or .NET**; use -[runtime-specific discovery](#runtime-specific-log-discovery). - -Verify a controlled request and its events before creating alerts. Traces can be sampled or -filtered, and an empty JSON result can mean a parser/pipeline mismatch. Check -[runtime collection defaults](APPLICATION-INSIGHTS.md#what-the-checked-in-sample-configures) -and [sampling limits](APPLICATION-INSIGHTS.md#sampling-privacy-and-collection-limits). +The JavaScript runtime writes JSON service events to `AppTraces.Message`. Confirm the format in +your deployment before using these queries. Python and .NET use different formats. ### Evaluation and delivery outcomes @@ -279,14 +143,10 @@ AppTraces | order by TimeGenerated asc ``` -Current JavaScript completion results are `evaluated`, `accepted`, or `failed`. -An early validation failure may remain `unclassified`; do not silently count it -as a live request. Evaluation validates the authorized encrypted request/nonce -path without sending an OTP. `accepted` means provider acceptance, **not handset -delivery**. Confirm delivery separately using controlled recipients and the -provider's supported delivery evidence. +Current completion results are `evaluated`, `accepted`, or `failed`. `accepted` means provider +acceptance, not recipient delivery. -### Request failures and background credential-refresh failures +### Request and credential failures ```kusto AppTraces @@ -306,30 +166,19 @@ AppTraces | order by TimeGenerated asc ``` -Background refresh warnings are emitted via `console.warn` without invocation -context. Verify their collection in your deployment; do not require an operation -ID or `functionName` to find them. Credentials can refresh independently of -evaluation requests. A successful evaluation does not prove the credentials are -usable, and no refresh warnings do not prove success when caching/background -refresh is disabled. - -For multiple regions, replace `AppTraces` with explicit, separately scoped -workspace branches as in the request query, add a region label to each branch, -and include it in the final grouping. Do not combine copies of the same event -exported through multiple pipelines. - -### Runtime-specific log discovery +Credential refresh can run outside a request. A successful evaluation does not prove provider +credentials work, and no warning does not prove they are healthy when collection or refresh is off. -Do not search all languages for `logType: "request"` or parse every trace as JavaScript JSON. -Current runtimes emit fixed completion events, with different formats: + +## Runtime log formats -| Runtime | Emission in source | Fields to verify in the deployed trace | -|---|---|---| -| JavaScript | JSON string from [logging.js](../javascript/src/functions/logging.js): `logType: "service"`, `eventName: "request_completed"`. | camelCase `functionRequestId`, `httpStatus`, `result`; parse `Message` using the JavaScript queries above. | -| Python | [otp_log.py](../python/src/otp_log.py): fixed message `OTP request completed`; logging extras include `event_name`, `event_id`, `httpStatus`, `result`, `functionRequestId`. | Whether extras survive in `Properties`, and their actual names. A message alone does not retain its status/result. | -| .NET | [OtpLog.cs](../dotnet/Src/OtpLog.cs): typed `ILogger` event `request_completed` (1401), message beginning `Request completed: HTTP`. | `HttpStatus`, `Result`, `ElapsedMs` and `FunctionRequestId` scope, if preserved by the exporter. Event ID/name may be separately exported or absent. | +| Runtime | Completion event | What to verify | +| --- | --- | --- | +| JavaScript | JSON `request_completed` service event | Parse `Message` and use camelCase fields such as `httpStatus` and `result`. | +| Python | Message `OTP request completed` with logging extras | Check whether `event_name`, `result`, and `httpStatus` appear in `Properties`. | +| .NET | Event `request_completed` (1401), message starts with `Request completed: HTTP` | Check whether `Result`, `HttpStatus`, `ElapsedMs`, and request scope fields appear in `Properties`. | -First discover property **names**, without dumping message bodies or values: +Discover property names without returning message bodies: ```kusto AppTraces @@ -341,310 +190,160 @@ AppTraces | order by StoredRows desc ``` -No property rows does not prove no traces: the exporter may have sent only formatted messages. -Use the [coverage query](APPLICATION-INSIGHTS.md#read-only-queries-to-establish-what-is-present) -to distinguish absent properties from absent logs. Inspect a controlled nonproduction invocation -privately; exporters can prefix or rename structured properties. +Exporter configuration can rename or remove structured properties. Validate one controlled +nonproduction invocation before creating an alert. See the [log field reference](LOGGING.md) for +event order, identifiers, and safe fields. + + +## Multiple regions -**Python example:** use this only if `event_name`, `result`, and `httpStatus` are preserved as -top-level `Properties` keys. The fixed-message branch makes missing completion properties -visible instead of silently dropping those rows: +If regions share a workspace, query `AppRequests` once and group by `_ResourceId` and +`AppRoleName`. For separate workspaces, use explicit references and label each branch: ```kusto -AppTraces -| where TimeGenerated >= ago(1h) -| where _ResourceId =~ "" - and AppRoleName == "" -| where tostring(Properties.event_name) == "request_completed" - or Message == "OTP request completed" -| extend Result = tostring(Properties.result), HttpStatus = toint(Properties.httpStatus) -| extend SchemaStatus = iff(isempty(Result) or isnull(HttpStatus), - "missing completion properties", "properties present") -| summarize StoredRows = count() by SchemaStatus, Result, HttpStatus +union + (workspace("").AppRequests + | where TimeGenerated >= ago(1h) + | where _ResourceId =~ "" + and AppRoleName == "" and Name == "SendOtp" + | extend Region = "region-a"), + (workspace("").AppRequests + | where TimeGenerated >= ago(1h) + | where _ResourceId =~ "" + and AppRoleName == "" and Name == "SendOtp" + | extend Region = "region-b") +| extend Weight = tolong(coalesce(ItemCount, 1)) +| summarize Requests = sum(Weight), + Failed = sumif(Weight, Success == false), + UnknownOutcome = sumif(Weight, isnull(Success)), + P95Ms = percentilew(DurationMs, Weight, 95), + P99Ms = percentilew(DurationMs, Weight, 99) + by Region, _ResourceId, AppRoleName, bin(TimeGenerated, 5m) +| extend FailurePct = 100.0 * Failed / Requests +| order by TimeGenerated asc, Region asc ``` -**.NET example:** use the emitted completion-message prefix to find candidate rows, then verify -the structured keys. If the exporter prefixes keys, change the property accessors accordingly; -do not assume missing status equals zero or success. +Do not average regional percentages or percentiles. Keep inaccessible workspaces and query errors +visible. A healthy region must not hide another region's missing data or failures. + +### Missing telemetry + +Use an explicit expected inventory when each listed origin should receive requests: ```kusto -AppTraces -| where TimeGenerated >= ago(1h) -| where _ResourceId =~ "" - and AppRoleName == "" -| where Message startswith "Request completed: HTTP" -| extend Result = tostring(Properties.Result), HttpStatus = toint(Properties.HttpStatus) -| extend SchemaStatus = iff(isempty(Result) or isnull(HttpStatus), - "missing completion properties", "properties present") -| summarize StoredRows = count() by SchemaStatus, Result, HttpStatus +let Expected = datatable(Region:string, Component:string, Role:string) +[ + "region-a", "", "", + "region-b", "", "" +] +| extend Component = tolower(Component); +let Observed = + union workspace("").AppRequests, + workspace("").AppRequests + | where TimeGenerated between (ago(20m) .. ago(5m)) + | where Name == "SendOtp" + | summarize StoredRows = count(), LastSeen = max(TimeGenerated) + by Component = tolower(_ResourceId), Role = AppRoleName; +Expected +| join kind=leftouter Observed on Component, Role +| extend StoredRows = coalesce(StoredRows, tolong(0)) +| extend MissingTelemetry = StoredRows == 0 +| project Region, Component, Role, StoredRows, LastSeen, MissingTelemetry ``` -These are **stored rows**, not traffic totals or delivery receipts. Verify the release's schema and -collection before creating alerts. Missing events require investigation, not a healthy status: -.NET host OpenTelemetry does not initialize a direct worker exporter, and malformed .NET binding -can fail before handler events. See [collection configuration](APPLICATION-INSIGHTS.md#what-the-checked-in-sample-configures). +For an alert, append `| where MissingTelemetry` and trigger when table rows are greater than zero. +Use this only when traffic or an authorized scheduled test is expected. Idle or standby origins can +legitimately have no request rows. -For incidents, correlate `request_failed` stage/reason/status with provider-response events. -Use the exported host `OperationId`/invocation and application request IDs, not OTPs, nonces, or bodies. -Keep Microsoft/provider IDs distinct; some runtimes omit provider references. -An upstream `200` can precede a body-read timeout and Function `504`. -See [event order and identifier provenance](LOGGING.md). +## Alerts -`credential_refresh_failed` also differs operationally: JavaScript/Python have a polling -refresh loop, while .NET emits this event on failed startup/cache-fill acquisition and has no -periodic poller. See [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). -Absence of a warning, especially in an idle app or a missing export pipeline, is not a credential -health check. +1. In **Azure Monitor > Alerts > Action groups**, create the notification route and test it. +2. Create one rule per clear operational condition. +3. Preview the query over healthy, failing, and empty periods. +4. Test firing, notification, and resolution in nonproduction. +5. Record the owner, severity, threshold, maintenance process, and runbook. -## 5. Set up notifications and alert rules - -Start in nonproduction with an operator who has rule-write permissions, not just data access. -Agree ownership, severity, escalation, and maintenance handling. +| Starting alert | Scope and condition | +| --- | --- | +| Request failures | Five-minute `AppRequests` summary with a minimum request count and a threshold based on normal traffic. | +| High latency | Five-minute request summary with a reviewed p95 threshold and minimum request count. | +| JavaScript credential failures | `credential_refresh_failed` events over a 15-minute window. | +| Missing regional telemetry | Expected-inventory query filtered to `MissingTelemetry`. | +| Certificate or credential renewal | Independent inventory reminder and the approved renewal runbook. | -### Action groups +Use **Custom log search** for KQL rules and verify the rule identity can read every workspace. +Use Function metrics only after confirming that the signal exists on the deployed FC1 or EP1 plan. +Tune all sample thresholds to measured traffic and costs. -In **Azure Monitor > Alerts > Action groups > Create**, choose the resource group, name, and -residency-appropriate processing region. Add on-call email and any secured incident integration; -avoid depending on the monitored OTP provider. Use the common alert schema where supported. -Test notification routing before attaching production rules; that test does not verify the alert query. + +### Operational ownership -For multiple regions, choose global/supported regional processing so one failed app region cannot -disable the only response path. See [action group setup][action-groups]. +Assign owners for certificate renewal, provider credentials, alert routing, telemetry retention, +and cost. Follow the [certificate lifecycle runbook](../setup/docs/README.md#encryption-certificate-lifecycle). +A new Key Vault certificate version does not update the Function's pinned reference or Microsoft +Entra ID registration automatically. -### Metric alerts +Guided setup does not create action groups, alerts, Workbooks, availability tests, or certificate +contacts. Cost budgets notify but do not stop spending. -Open the **Function App > Metrics**, choose its metric namespace, and verify the -signal exists and has data on the actual FC1 or EP1 plan. Available signals and -dimensions vary by hosting plan; never substitute an unavailable metric with zero. -Create the alert from **Alerts > Create > Alert rule**, set the scope, signal, -aggregation, window, evaluation frequency, dimensions, and action group. +## Workbook -As a starting point, where the selected resource exposes the HTTP 5xx metric, -alert on **Total > 5 over 5 minutes**, evaluated every minute. Use Application -Insights request failure rules below when that platform signal is unavailable or -when the `SendOtp` operation filter is required. Add plan-appropriate execution, -memory, or instance signals only after establishing a baseline; there is no -single CPU/instance threshold appropriate to both FC1 and EP1. + -For multiple regions, use one scoped rule per Function, or supported multi-resource -rules with resource-specific alert instances. Keep regional failures visible. -Add Service Health/Resource Health notifications and review Key Vault failures or -throttling, storage availability/latency, and credential/certificate expiry through -their respective resource monitoring. A successful Function response alone does -not validate those dependencies' future readiness. +Create a Workbook only after the queries are verified. Include: - -### Operational ownership +- request volume, failure percentage, and p95/p99 latency +- expected origins and last-seen status +- validated runtime completion and credential events +- current alerts, deployment version, and owners -Keep `certificateExpiresUtc`, vault certificate/secret versions, and Entra encryption credential -expiry in a restricted inventory with a renewal owner and backup. Follow the -[renewal runbook](../setup/docs/README.md#encryption-certificate-lifecycle) for vault-wide contacts, -`EmailContacts`, and reused-certificate policies. **Contacts do not renew certificates; a new vault -version does not update the Function's pinned reference or Entra.** - -Set an independent reminder before the 30-day near-expiry window and rehearse approved renewal -in nonproduction. For Key Vault near-expiry/new-version events, configure an -[Event Grid subscription and handler](https://learn.microsoft.com/azure/key-vault/general/event-grid-overview) -and test its notification path. Diagnostic settings alone do not create these alerts, and those -events do not automatically become Application Insights traces. - -For provider API keys, track the provider's expiration/revocation policy and verify a coordinated -matching-key/customer-ID transition. For Soprano, monitor failed credential acquisition and provider -authorization failures; do not treat short-lived access-token renewal as certificate renewal. -Evaluation is insufficient for either provider's delivery readiness. - -Review telemetry volume, effective retention, and daily caps; caps can create blind spots, not just -reduce cost. Configure Azure Cost Management budgets/notifications for the chosen scope and review -provider billing separately. Budgets do not stop spending. Assign operators to act on alerts and -periodically verify routing, firing, and resolution rather than only creating rules once. - -### Log search alerts - -In **Azure Monitor > Alerts > Create > Alert rule**, select the workspace and -**Custom log search**. Paste an adapted query, preview its results, configure the -condition and action group, and save only after reviewing estimated charges. -Use an explicit query lookback consistent with the query's time filter. - -For the request summary examples, change the filter to `ago(5m)` in every source -and remove `bin(TimeGenerated, 5m)` from the grouping for **one summary -row per origin over the whole rolling window**. This avoids evaluating a partial -calendar bucket as if it were a complete five-minute interval. For the single -region query, remove the grouping entirely. Remove the final time sort in both -queries, since the rolling summaries no longer return `TimeGenerated`. Keep -component/role/region grouping for the multi-region query. - -| Starting rule | Query adaptation | Condition | -| --- | --- | --- | -| Elevated request failure rate | Append a `where` filter with `Requests >= 20 and FailurePct > 5` to the rolling-window summary. | Table rows > 0; evaluate every 5 minutes; start with one violating evaluation. | -| High request latency | Append a `where` filter with `Requests >= 20 and P95Ms > 2000` to the same summary. | Table rows > 0; evaluate every 5 minutes; start with one violating evaluation. | -| JavaScript credential failures | Use the service-event query with `ago(15m)` and only `credential_refresh_failed`. | Table rows > 0; evaluate every 5 minutes; start with a warning. | -| Expected origin missing | Append a `where MissingTelemetry` filter to the inventory query. | Table rows > 0; 20-minute lookback; evaluate every 5 minutes after accounting for ingestion delay. | - -The minimum request count deliberately avoids unstable percentages on tiny -samples; it also means low-traffic failures need separate investigation or -authorized synthetic monitoring. Tune thresholds and severity to observed -traffic, provider behavior, and your error budget. Use dimension splits for -regional rules; do not configure a numeric threshold on an already filtered -table-row query as though it returned raw events. - -These rolling summaries no longer return a datetime column. Do not select multiple violations -for them as written: [Azure Monitor requires datetime output][log-alerts] for that option. -For a sustained-failure rule, have the query maintainer design time-bucketed output and align -the query lookback, aggregation granularity, and evaluation period before enabling it. - -Verify cross-workspace access for the alert's evaluation identity, enable -appropriate resolution behavior, and inspect rule health. Query/permission -failures must be visible to operators. A Workbook that loads under your own -account does not verify the rule identity. Test firing, notification, and -resolution using controlled nonproduction conditions, then document a runbook. -Use alert processing rules for planned maintenance rather than silently removing -origins from inventory. See [log alert configuration][log-alerts]. - -## 6. Build a shared operations Workbook - -Open **Azure Monitor > Workbooks > New > Edit**. Add a text panel describing the -environment, ownership, evaluation/live distinction, and missing-data behavior. -Add query panels using the correct workspace scope: - -1. Regional request volume and failure percentage from the request summaries. -2. Regional p95/p99 latency, with units in milliseconds and request counts beside it. -3. Expected-origin inventory, last-seen time, and an explicit missing-data state. -4. JavaScript evaluated/accepted/failed events and credential warnings, only where - that parser is verified; otherwise show the runtime-specific validated view. -5. Relevant Function/dependency metric charts, current alerts, and deployment context. - -Scope to the chosen workspace/component/role; multiple regions need explicit sources or scoped -shared-workspace queries with regional grouping. Bind time/resource parameters into every source, -replacing hardcoded windows. Scheduled alerts cannot depend on Workbook-only parameters. -**Do not color an empty panel green.** - -Save to the approved resource group and verify both Workbook and data access for readers. -Keep identifiers, exports, and screenshots private. See [Workbook authoring][workbooks]. +Bind every panel to the selected time and resource scope. Do not show an empty panel as healthy. +Workbook readers still need access to the underlying data. -## 7. Availability monitoring - -Keep `SendOtp` protected by Easy Auth for every probe and test. -An unauthenticated 401/403 demonstrates rejection, not a successful authorized -evaluation or provider call. Standard availability tests do not perform the -complete EPP token acquisition, encrypted-request creation, and nonce-validation -workflow automatically; a static bearer token header will expire. - -For simple HTTPS/TLS checks, a separately designed, non-delivering readiness -endpoint can be appropriate, subject to security review and network restrictions. -This sample does not provide such an endpoint. Its readiness contract must not -send OTPs, expose secrets, or claim delivery success. A static 200 response only -proves that that route answered. - -For the actual authentication/decryption path, use the supported EPP test -procedure and an authorized caller. An independently operated synthetic runner -must be able to satisfy existing caller restrictions, acquire fresh credentials, -construct a valid encrypted evaluation request, and verify the nonce without -recording it. If the runner cannot meet those caller requirements, document the -monitoring gap and retain the approved Easy Auth allowlist. -Record sanitized success/duration results through your approved monitoring -pipeline and monitor the runner's schedule/failures separately. - -Evaluation (`mode: 2`) is non-delivering and does not test provider acceptance. -Use separately approved, rate-limited live tests with controlled recipients when -delivery evidence is required; account for provider costs and avoid unsolicited -messages. Availability alerts should distinguish endpoint rejection, evaluation -failure, provider acceptance failure, and missing synthetic results. - -FC1 is configured by setup with zero always-ready instances and can scale to zero. -No requests or background refresh logs while idle are not inherently failures. -Cold starts affect latency; frequent probes can keep instances active and change -both costs and the latency baseline. Measure both warm traffic and the first -authorized request after idle. Do not promise cold-start-free operation or change -always-ready capacity without a separate hosting/cost decision. - -For multiple regions, test each required regional path through its permitted -network/authentication route and the public client path separately. A global -endpoint can remain healthy while an origin is broken; an origin can also be -healthy but unused. Do not open origin ingress to bypass its access restrictions. -Review the current [availability test capabilities][availability] before choosing -Standard tests or a custom authenticated runner. +## Availability + +Keep `SendOtp` protected by Easy Auth. An unauthenticated `401` or `403` confirms rejection, not +endpoint availability. + +The supported end-to-end check uses an authorized caller, a valid encrypted evaluation request, +and nonce verification without recording the nonce. Evaluation does not test provider acceptance +or delivery. Live checks need separate approval, controlled recipients, and cost limits. + +FC1 can scale to zero. Idle periods can have no requests or refresh logs, and the first authorized +request can include cold-start latency. Frequent probes can change both cost and observed latency. -## 8. Front Door monitoring - -Use this layer with the [Front Door multi-region design](FRONTDOOR.md). -For Standard/Premium, inspect the profile's Metrics for **Origin Health Percentage**, -**Origin Latency**, **Request Count**, and response/error signals supported by the -selected metric namespace. Confirm units and available origin/origin-group -dimensions before choosing thresholds. For example, sustained origin health below -100% over 5 minutes can start as a warning, not a universal outage definition. -Keep Function request latency (milliseconds) distinct from edge/origin metric units. - -In the profile's **Diagnostic settings**, an operator can route access, health-probe, -and WAF logs where supported to the approved workspace. These Front Door logs are -not enabled automatically by the Function setup. Review existing settings first -to avoid duplicate export, agree on retention and ingestion cost, and inspect -the destination schema before writing log queries. Table/column names depend on -the diagnostic destination mode; do not assume `AppRequests` contains edge logs. - -Use origin-dimensional health and probe failures to diagnose routing, access-log -status/latency for client-visible errors, and WAF logs for legitimate requests -blocked at the edge. Keep request URIs, IP addresses, and tracking references -private. Probe logs alone are not a reliable success counter; use the health -metric and the documented logging semantics. - -Match the expected-origin inventory against regional Function telemetry as well -as edge observations. Front Door probes do not perform an authenticated encrypted -`SendOtp` evaluation or prove provider delivery. A probe path must follow the -separate readiness design described above. See [Front Door monitoring][front-door]. +## Front Door - -## 9. Sampling and data quality +For the [multi-region design](FRONTDOOR.md), monitor Front Door separately from the Functions: -If data stops arriving, first check the time range, workspace, component, role filters, -and actual traffic. Then check recent deployments, log levels, host and worker exporters, -ingestion access, network access, sampling, throttling, and workspace daily caps. -Retention and collection transformations can also affect query results. Treat an -inaccessible workspace or missing table as a collection issue, not a healthy empty window. +- health-probe status and origin health percentage +- origin latency and edge response status +- WAF or access-log rejections +- each regional Function, dependency, and telemetry source -This query estimates the event-to-ingestion gap for **received** requests: +Origin health does not prove caller authentication, provider access, or delivery. Keep origin +access restrictions and Easy Auth enabled while investigating routing. -```kusto -AppRequests -| where TimeGenerated >= ago(1h) -| where _ResourceId =~ "" - and AppRoleName == "" -| extend GapSeconds = datetime_diff("second", ingestion_time(), TimeGenerated) -| where isnotnull(GapSeconds) -| summarize P95GapSeconds = percentile(GapSeconds, 95), - MaxGapSeconds = max(GapSeconds), StoredRows = count() -``` - -Request timestamps represent request start, so this gap can include execution -duration and clock differences as well as export/ingestion delay. It cannot -detect records that never arrived, and `ingestion_time()` can be unavailable. -Use it with the inventory and independent test schedule, not instead of them. - -Do not log or publish phone numbers, OTP/message bodies, nonce values, encrypted -request payloads, tokens, private keys, raw provider responses, or sensitive URL -query strings. Restrict telemetry access and export privileges. Even sanctioned -correlation IDs, resource IDs, and sanitized logs can be private operational data. -Use aggregate projections in shared reports instead of raw messages. - -Before relying on these examples, validate the deployed schemas, filters, and -query results in your own nonproduction workspace. Exercise normal requests, -controlled failures, evaluation versus live requests, an entirely empty observation -window, and one missing region. Confirm both action-group delivery and actual -alert firing/resolution. These freshly authored examples have been reviewed against -repository code and Microsoft schema documentation, **not executed against a live -workspace as part of this guide**. - -Schema references: [AppRequests][apprequests], [AppTraces][apptraces], and -[Azure Functions monitoring][functions-monitoring]. - -[apprequests]: https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/apprequests -[apptraces]: https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/apptraces -[functions-monitoring]: https://learn.microsoft.com/en-us/azure/azure-functions/monitor-functions -[cross-workspace]: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cross-workspace-query -[action-groups]: https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups -[log-alerts]: https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule -[workbooks]: https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-create-workbook -[availability]: https://learn.microsoft.com/en-us/azure/azure-monitor/app/availability -[front-door]: https://learn.microsoft.com/en-us/azure/frontdoor/monitor-front-door + +## Collection and data quality + +- **Missing data is not success.** Check resource scope, role name, table, time range, access, + ingestion delay, filters, sampling, caps, retention, and exporter configuration. +- **Sampling changes counts.** Use `ItemCount` where available. Weighted totals remain estimates. +- **Pipelines differ.** Host requests, worker logs, resource metrics, diagnostics, and Microsoft + Entra sign-ins are separate data sources. +- **Protect customer data.** Do not collect phone numbers, OTPs, bodies, encrypted payloads, + tokens, credentials, private keys, nonces, or raw provider responses. +- **Test the monitor.** Validate the query, rule identity, action group, notification, resolution, + and missing-data behavior in nonproduction. + +For pipeline setup and missing-ingestion checks, use +[Application Insights](APPLICATION-INSIGHTS.md). For endpoint failures, use +[troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage). + +[action-groups]: https://learn.microsoft.com/azure/azure-monitor/alerts/action-groups +[log-alerts]: https://learn.microsoft.com/azure/azure-monitor/alerts/alerts-create-log-alert-rule +[workbooks]: https://learn.microsoft.com/azure/azure-monitor/visualize/workbooks-overview diff --git a/docs/MULTI-PROVIDER-IMPLEMENTATION.md b/docs/MULTI-PROVIDER-IMPLEMENTATION.md index 459d25f..605b27e 100644 --- a/docs/MULTI-PROVIDER-IMPLEMENTATION.md +++ b/docs/MULTI-PROVIDER-IMPLEMENTATION.md @@ -4,19 +4,20 @@ [Documentation](README.md) | [Normal deployment](GETTING-STARTED.md) | [Provider interface](../TECHNICAL.md#provider-result-mapping) -Add controlled primary-to-secondary fallback around `/api/SendOtp`. Keep one server-owned SMS -provider order with durable state and provider-specific approval. +This advanced design adds controlled primary-to-secondary fallback around `/api/SendOtp`. Use the secondary **only after proof that the primary did not accept the request**. Timeouts, lost responses, and generic errors are uncertain and must not trigger an automatic resend. -Follow [policy](#1-set-a-fixed-provider-order-and-a-deny-by-default-policy), -[account configuration](#2-configure-and-prevalidate-both-accounts), -[credential isolation](#3-give-each-context-its-own-credential-service-and-lifecycle), -[durable state](#4-add-durable-operation-state-before-enabling-fallback), -[fallback flow](#5-integrate-one-guarded-fallback-into-the-handler), -[end-to-end deadline](#6-enforce-one-aggregate-deadline), and [validation](#7-validate-offline-then-deploy-only-after-separate-approval). -[.NET/Python pointers](#net-and-python-integration-pointers) are not implemented fallback. +| Step | Task | +| --- | --- | +| 1 | [Set provider order and policy](#1-provider-order) | +| 2 | [Configure both accounts](#2-configure-and-prevalidate-both-accounts) | +| 3 | [Isolate credentials](#3-credential-isolation) | +| 4 | [Add durable operation state](#4-operation-state) | +| 5 | [Implement guarded fallback](#5-fallback-flow) | +| 6 | [Apply one deadline](#6-use-one-end-to-end-deadline) | +| 7 | [Test and deploy](#7-validation-and-deployment) | ## 1. Provider order @@ -45,15 +46,15 @@ Keep **routing order** separate from **fallback eligibility**. Use this policy: | Primary-only credential acquisition fails before transport is entered | Eligible only if this pre-dispatch fact is recorded, the account policy explicitly permits it, the durable operation guard allows the transition, and the request budget is sufficient. | | Provider-specific, documented proof that the primary did not accept the operation | Eligible only through a reviewed nonacceptance classifier, explicit account policy, the durable guard and sufficient budget. | -**Enable fallback only after the provider confirms a safe nonacceptance signal.** Caller -authentication and provider account controls still apply. +Enable fallback only after the provider confirms a safe nonacceptance signal. Caller +authentication and provider controls still apply. ## 2. Configure and prevalidate both accounts [config.js](../javascript/src/functions/config.js) accepts `readConfig(settings)`, so each account can use its own immutable settings without changing `process.env` per request. -The following **customer-owned format** is not loaded by the sample. Replace each -`` with a registered adapter and match its credential requirements. +The following customer-owned format is not loaded by the sample. Replace each +`` with a registered adapter. ```json [ @@ -81,9 +82,8 @@ The following **customer-owned format** is not loaded by the sample. Replace eac ] ``` -This SMS-only policy requires `EPP_PROVIDER_SMS_ENDPOINT` for each account. A voice policy requires -`EPP_PROVIDER_VOICE_ENDPOINT` instead. The validated request channel selects the matching endpoint; -there is no shared or other-channel endpoint fallback. +This SMS policy requires `EPP_PROVIDER_SMS_ENDPOINT` for each account. Voice uses +`EPP_PROVIDER_VOICE_ENDPOINT`. There is no shared endpoint fallback. At startup, validate both account configurations before enabling the endpoint. Check for unique IDs, the required channel endpoint, matching authentication mode, approved URLs, credential diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 59834cf..945e8ae 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -1,216 +1,191 @@ -# Customer configuration and validation +# Configure and validate the deployment -[Documentation](README.md) | [First deployment](GETTING-STARTED.md) | [Troubleshooting](../setup/docs/Troubleshooting.md) +[Documentation](README.md) | [Deployment guide](GETTING-STARTED.md) | [Troubleshooting](../setup/docs/Troubleshooting.md) -After deployment, complete [provider authentication](#complete-provider-authentication), then -[validate the endpoint](#validate-the-deployed-endpoint). +Use this page after deployment to connect the provider, validate the endpoint, and hand it to +operations. | Task | Go to | | --- | --- | -| Confirm access before spending | [Eligibility and approved procedures](#before-purchasing-or-deploying) | -| Find or check setup values | [Inputs](#inputs-you-supply-to-setup), [setup-managed values](#values-setup-configures-for-you) | -| Connect the provider | [Credential names and handoff](#complete-provider-authentication) | -| Test and activate | [Acceptance checks](#validate-the-deployed-endpoint), [operations handover](#activate-operate-and-retire) | -| Build a custom package | [Custom development and deployment](#custom-development-and-manual-deployment) | +| Confirm access before spending | [Before deployment](#before-purchasing-or-deploying) | +| Gather setup values | [Values and ownership](#values-and-ownership) | +| Connect Soprano or Telesign | [Provider authentication](#complete-provider-authentication) | +| Run approved tests | [Validate the endpoint](#validate-the-deployed-endpoint) | +| Activate and assign owners | [Operations](#activate-operate-and-retire) | +| Deploy custom code | [Custom deployment](#optional-local-development-and-manual-deployment) | -## Before purchasing or deploying + +## Before deployment -In [Microsoft Security Store](https://securitystore.microsoft.com/private-solutions), confirm -account activation, sender registration, countries, channels, pricing, and EPP access. +In [Microsoft Security Store](https://securitystore.microsoft.com/private-solutions), confirm the +provider account, sender registration, countries, channels, pricing, and EPP access. ![Cropped examples of phone-provider offer cards in Microsoft Security Store](images/onboarding/security-store-providers.png) -*Sample cards only. Check [guided provider support](#provider-credential-names).* +*Sample cards only. Check the current offer and [guided provider support](#provider-credential-names).* -Before deployment, confirm eligibility, licensing, preview access, an authorized test caller, -and Microsoft's activation procedure. Use the offer's support route or Microsoft support for -missing access. Azure subscription ownership does not grant EPP access. +Before spending or deploying, confirm: -Assign an **onboarding owner** to coordinate tenant policy, Azure, the provider, and Microsoft support. +- eligibility, licensing, and preview access +- the authorized Microsoft test caller and procedure +- the activation process +- an onboarding owner for Microsoft Entra ID, Azure, provider access, and support -Use [production readiness](../TECHNICAL.md#production-readiness) to plan the -capabilities your live service needs, such as durable delivery, retry and deduplication strategy, -an end-to-end deadline, certificate renewal, and delivery confirmation. +Azure subscription ownership does not grant EPP access. Use the offer's support route or Microsoft +support when access is missing. -## Values and ownership +Before live use, review [production readiness](../TECHNICAL.md#production-readiness), including +delivery evidence, retry and duplicate-send handling, deadlines, monitoring, and certificate +renewal. -### Inputs you supply to setup +## Values and ownership -Use values from the named portal pages. The -[noninteractive example](../setup/docs/README.md#noninteractive-example) uses the same parameters. + +### Values you provide -| Input | Where to find or decide it | Where to set it / required for | -|---|---|---| -| Customer tenant ID | Microsoft Entra admin center > Overview > Tenant ID; must contain the dedicated app and the Azure identities. | `-TenantId`; all deployments. Not the provider tenant ID. | -| Azure subscription ID | Azure portal > Subscriptions > selected subscription > Overview. Confirm its directory is the customer tenant. | `-SubscriptionId`; all deployments. Not a tenant ID. | -| Endpoint application client ID | Entra > App registrations > the dedicated app > Overview > Application (client) ID. Select the registration by this ID, not just its display name. | `-ApplicationId`; all deployments. **Not** the app Object ID or Enterprise application's Object ID. | -| Azure region | Subscription's available Linux FC1/EP1 regions and quota, plus your organization's requirements. | `-Location`, for example `westus2`; all deployments. | -| Provider | Provider's approved account integration: guided catalog supports `telesign` or `soprano`. The endpoint accepts both SMS and voice, selected by each request. | `-Provider`; all deployments. No channel selection is needed in setup. | -| Provider route label | Provider-confirmed Global/EU selection; see the caveat below. | `-EndpointRegion global` or `eu`; all deployments. The prompt calls this **Tenant scope**. | -| Language / hosting | One of `javascript`, `dotnet`, `python`; FC1 or EP1 after reviewing billing and cold starts. | `-Language`, `-ServicePlan`; all deployments. | -| Resource prefix | A unique deployment label, 2-8 lowercase letters/digits, starting with a letter. | `-ResourcePrefix`; all deployments. Keep it with your inventory for reruns. | -| Encryption certificate choice | Setup lists the application's encryption key IDs, thumbprints, and validity. Use the saved `encryptionKeyId` or the app manifest's `keyCredentials` to identify the credential to keep. | `-CertificateAction Reuse` or `Replace` when certificates already exist. Unattended reuse also requires `-EncryptionKeyId` and `-KeyVaultCertificateId`. See [certificate selection](../setup/docs/README.md#encryption-certificate-selection). | -| Existing certificate location | Azure portal > Key Vault > Certificates > certificate > exact version > Certificate Identifier. Use the `/certificates/` URL, not Show Secret Value. | `-KeyVaultCertificateId`; reuse only. Add `-KeyVaultSubscriptionId` when that vault is in a different subscription in the same customer tenant. | -| Output directory / versions | A private local folder; optionally a reviewed source commit and published release tag. | `-OutputDirectory`, `-SourceRef`, `-PackageReleaseTag`; optional. Keep launcher and support source aligned. | +| Input | Source | Setup option | +| --- | --- | --- | +| Customer tenant ID | Microsoft Entra admin center > Overview | `-TenantId` | +| Azure subscription ID | Azure portal > Subscriptions | `-SubscriptionId` | +| Endpoint application client ID | Entra > App registrations > application > Overview | `-ApplicationId` | +| Azure region | Available Linux FC1/EP1 region that meets quota and organization requirements | `-Location` | +| Provider | Approved Security Store integration | `-Provider telesign` or `soprano` | +| Provider route | Provider-confirmed Global or EU route | `-EndpointRegion global` or `eu` | +| Runtime and plan | JavaScript, .NET, or Python with FC1 or EP1 | `-Language`, `-ServicePlan` | +| Resource prefix | Unique 2-8 character lowercase label that starts with a letter | `-ResourcePrefix` | +| Encryption certificate | Existing registered key or approved replacement | `-CertificateAction` and certificate options | +| Output and source versions | Private output folder and reviewed source/release versions | Optional output and version options | -**Global/EU selects provider routing, not Azure placement or residency.** Current Telesign and -Soprano profiles use the same Global/EU URLs; Soprano also shares its app ID and scope. Confirm -provider data locations separately. +Use the application **client ID**, not its Object ID. The customer tenant ID and provider tenant ID +are different values. Global or EU selects provider routing, not Azure location or data residency. -**One guided deployment has one provider and both channels.** Validate each channel separately. -Changing provider or region with the same subscription, app, and prefix reconfigures that -endpoint. Use a new app and prefix for an independent endpoint. See [Front Door](FRONTDOOR.md) -for regional origins. +One guided deployment uses one provider and supports both SMS and voice. Each request selects its +channel. Reusing the same subscription, application, and prefix reconfigures that deployment. -### Values setup configures for you +See the [setup reference](../setup/docs/README.md#prerequisites-for-step-2) for complete parameters +and [certificate selection](../setup/docs/README.md#encryption-certificate-selection). + -Keep `deployment-*.json` and the public certificate in private storage. Compare the summary with -the approved plan. `policyChanged: false` is expected. - -Inspect settings in **Function App > Settings > Environment variables**. Keep setup-managed -values out of tickets and avoid copying settings from another app. - -| Value or setting | Source / verification | Customer action | -|---|---|---| -| `EPP_PROVIDER_NAME`, `EPP_PROVIDER_ENDPOINT_REGION`, `EPP_PROVIDER_AUTH_MODE` | Selected profile: Telesign `apiKey`, Soprano `oauth`. | Compare with approved provider/region. | -| `EPP_PROVIDER_SMS_ENDPOINT`, `EPP_PROVIDER_VOICE_ENDPOINT` | Complete SMS and voice URLs from the selected regional profile; they may differ. Setup writes both, and each request's `channel` selects one. | Have the provider confirm account access for each intended channel. Do not append another API path or substitute an arbitrary test URL. | -| `EPP_PROVIDER_TENANT_ID` | Profile's provider directory; also appears as `providerTenantId` in the summary. | Do not replace it with the customer's tenant ID. Used for provider tenant restriction and Soprano OAuth authority. | -| `EPP_PROVIDER_APP_ID`, `EPP_PROVIDER_SCOPE` | Soprano API resource client ID and exact `api://.../.default` scope from the profile. | Provider administrator confirms these; API-key providers do not need them. The app ID is setup metadata; the runtime requests the configured scope. Neither is the endpoint app ID. | -| `EPP_OUTBOUND_CLIENT_ID` | Dedicated endpoint application's **client ID**, reused as the Soprano calling app. | Do not create a second client secret or substitute the Soprano API app ID. | -| `EPP_OUTBOUND_MI_CLIENT_ID` | Setup-created outbound user-assigned managed identity > Overview > Client ID. | Soprano token exchange uses this ID. Its Principal/Object ID is different and is used by the federated credential's subject. | -| `KEY_VAULT_URL` | Summary's `resources.keyVault`; vault Overview > Vault URI. | Put Telesign credentials in this vault. Setup grants its Function system identity Key Vault Secrets User. | -| `EPP_DECRYPTION_KEY_PEM`, `EPP_ENCRYPTION_KEY_ID` | Versioned Key Vault reference and the single registered encryption credential ID. The certificate can be in a reused vault distinct from `KEY_VAULT_URL`. Summary includes certificate/secret identifiers and expiry, **not** private-key bytes. | Do not view/copy the private key. Assign a [renewal owner](../setup/docs/README.md#encryption-certificate-lifecycle). | -| `EPP_PROVIDER_TIMEOUT_MS`, `EPP_PROVIDER_RETRY_INTERVAL_MS` | Profile timing values. Runtime provider HTTP timeout is capped at 2500 ms. | Neither is a whole-request deadline; retry interval metadata does **not** enable send retries. | -| `EPP_KEY_VAULT_CACHE_ENABLED`, `EPP_ACCESS_TOKEN_CACHE_ENABLED` | Setup writes `false` for FC1, `true` for EP1. | Independently control API-key/OAuth caching and startup preparation. Unset defaults to `true`; deploy a supporting package and restart after changes. See [plan guidance](../setup/docs/README.md#service-plan-selection). | -| Application Insights, storage, runtime/package settings and identities | Created/configured for the selected plan; system identity handles vault/storage/telemetry, outbound identity handles Soprano exchange. | Verify telemetry ingestion. Do not copy local emulator settings or EP1-only settings into FC1. | -| Inbound caller issuer, audience and allowlist | Function App > Authentication; setup configures Easy Auth for the Microsoft phone-provider caller. | Read back platform authentication, not just `EPP_EXPECTED_*` metadata. App settings are not an alternative caller-authentication gate. | - -Guided setup writes both channel-specific endpoint settings and requires a package that reads -them. Manual configurations must also supply the endpoint for each intended channel; -a missing, empty, or invalid selected endpoint fails closed. - -The trust paths are separate: Easy Auth checks the caller, the Key Vault certificate decrypts -the JWE, and provider credentials authorize delivery. - -## Complete provider authentication +### Values setup configures +Keep `deployment-*.json` and the public certificate in private storage. Compare the summary with +the approved plan. `policyChanged: false` is expected because setup does not activate policy. + +| Setting | Purpose | Check | +| --- | --- | --- | +| `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 Soprano | Matches the selected profile | +| `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, `EPP_PROVIDER_SCOPE` | Soprano API tenant, app, and scope | Provider administrator confirms exact values | +| `EPP_OUTBOUND_CLIENT_ID`, `EPP_OUTBOUND_MI_CLIENT_ID` | Customer app and outbound identity used for Soprano 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 | +| `EPP_PROVIDER_TIMEOUT_MS`, `EPP_PROVIDER_RETRY_INTERVAL_MS` | Provider timing values | Retry interval metadata does not enable retries | +| `EPP_KEY_VAULT_CACHE_ENABLED`, `EPP_ACCESS_TOKEN_CACHE_ENABLED` | Credential caching | Setup uses `false` for FC1 and `true` for EP1 | +| Easy Auth issuer, audience, and caller allowlist | Inbound Microsoft caller validation | Read back the platform configuration | + +The trust paths are separate: + +- Easy Auth validates the caller. +- The Key Vault certificate decrypts the request. +- Provider credentials authorize the outbound delivery call. + +Settings select code that already exists. They do not install a provider adapter. + + -Guided setup supports **Soprano and Telesign**. Other bundled adapters require custom deployment. +## Provider authentication -| Provider | Credential names / mode | Deployment support | -|---|---|---| -| `infobip` | `infobip-api-key`; `Authorization: App ...` | Adapter only; no guided profile. Validate its account/options separately. | -| `sinch` | `sinch-api-token`; static token | Adapter only; no guided profile. Validate its account/options separately. | -| `soprano` | No API-key secrets; OAuth client assertion | Guided; complete the [provider-admin handoff](#soprano-provider-administrator-handoff). | -| `telesign` | `telesign-api-key` and `telesign-customer-id`; Basic authentication | Guided; complete the [vault steps](#telesign-enter-and-verify-the-two-vault-secrets). | +Guided setup supports **Soprano and Telesign**. -### Where to enter API-key credentials in Key Vault +| Provider | Authentication | Customer action | +| --- | --- | --- | +| 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 | -Use the vault named in the deployment summary: +Infobip and Sinch adapters are bundled but do not have guided setup profiles. Twilio requires the +[Add Twilio](BRING-YOUR-OWN-PROVIDER.md) development process. -1. Open the saved deployment summary and find `resources.keyVault`. -2. In **Azure portal > Key vaults**, open that vault. -3. Select **Objects > Secrets**. This list shows secret names and status, not their values. - Select **Generate/Import** to enter a credential. + +### Add Telesign secrets -![Key Vault Objects menu with Secrets selected and the Generate/Import action visible](images/onboarding/key-vault-secrets.png) +1. In the deployment summary, find `resources.keyVault`. +2. Open that vault in **Azure portal > Key vaults**. +3. Select **Objects > Secrets > Generate/Import**. -*The screenshot contains test examples. Create only the required secrets.* + ![Key Vault Objects menu with Secrets selected and the Generate/Import action visible](images/onboarding/key-vault-secrets.png) -4. Choose **Manual**. For **Name**, use the exact name in the provider table above. - For **Secret value**, paste the credential obtained through your provider's secure process. - Leave **Enabled** set to **Yes** and set activation/expiry dates to match your provider agreement. -5. Select **Create** only after checking the vault, name, and value. Repeat for any additional - required credential, such as an account/customer ID. +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**. ![Unsaved Create a secret form showing a sample name, masked placeholder value, and Enabled set to Yes](images/onboarding/key-vault-create-secret.png) -*Use the exact provider secret name. The shown value is fake.* +*The screenshots contain test examples and fake values.* -After saving, verify the name, enabled state, and version without viewing the value. Keep the -setup-created encryption certificate unchanged. +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. -### Telesign credentials - -1. Confirm account access to each intended channel's EPP URL. Use the - [vault steps above](#where-to-enter-api-key-credentials-in-key-vault) to store the raw API key as - `telesign-api-key` and matching raw Customer ID as `telesign-customer-id`. - Do not encode the pair, decode the key, or enter an Authorization header; the adapter builds Basic auth. -2. Verify enabled state, validity, and version metadata without **Show Secret Value**. - In **Access control (IAM)**, confirm the Function's **system-assigned identity** has - **Key Vault Secrets User**. Setup gives its Azure operator **Key Vault Secrets Officer**; - another credential administrator needs separate vault access, regardless of directory role. - For failures, allow RBAC propagation and check networking; never replace the encryption secret, - grant broad access, or copy credentials into app settings to bypass a denial. -3. Complete the [authorized evaluation and live checks](#validate-the-deployed-endpoint). - Evaluation does not validate these secrets. Setup disables provider caching for FC1 and enables - it for EP1. Plan first-use and rotation checks from - [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). - No warning is not proof of account authorization. - -Use portal secret entry rather than command-line literal values, transcripts, source files, -screenshots, or chat. During rotation, coordinate the matching pair and provider validity window; -two secret updates are not atomic. Keep old credentials usable during the approved transition -where the provider supports it, and verify live delivery before retiring them. +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. -### Soprano credentials +### Complete Soprano authorization -Setup creates customer-side federation, **not** the commercial account or access in Soprano's -tenant. Complete this handoff before treating OAuth as ready. +Setup creates the customer-side federation. The Soprano administrator still needs to authorize the +customer application and account. -Send **identifiers only** through the provider's approved onboarding channel: +Send identifiers only through the approved onboarding route: -- customer tenant ID and `applicationId` / `EPP_OUTBOUND_CLIENT_ID` -- intended channels and their full provider URLs +- customer tenant ID +- endpoint application client ID +- intended channels and provider URLs - `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, and `EPP_PROVIDER_SCOPE` -- the provider-requested account or sender reference +- outbound identity client ID and principal ID when troubleshooting federation +- provider-requested account or sender reference -For federation troubleshooting, also provide the outbound identity Client ID and Principal ID, -plus the federated credential's issuer, subject, and audience. Keep the managed-identity assertion, -access token, private key, and client secrets private. +Keep assertions, access tokens, private keys, and client secrets private. -Ask the provider administrator to confirm the API tenant, app, and scope. They can then authorize -the calling application's service principal through the provider's supported process and confirm -the exact consent, application roles, and account/channel access. Use the provider's approved -consent path rather than building a URL or role list from sample GUIDs. Record the completion -confirmation. +Ask the provider administrator to confirm the tenant, app, scope, consent or app roles, and account +access. Then inspect **App registrations > Certificates & secrets > Federated credentials**: -Inspect the dedicated app's **Certificates & secrets > Federated credentials** and the outbound -identity's Overview. Setup's subject is the identity's **Principal ID**, issuer is the customer's -tenant-specific v2 issuer, and audience is `api://AzureADTokenExchange`. Do not substitute the -identity Client ID into the subject. These are customer-side checks only; they do not prove -provider-side consent. See [Microsoft's federation guidance](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity) -for the mechanism, then use an authorized controlled live test to verify the full integration. +- issuer is the customer tenant-specific v2 issuer +- subject is the outbound identity's **Principal ID** +- audience is `api://AzureADTokenExchange` -## Validate the deployed endpoint +The identity client ID is not the federation subject. Customer-side configuration alone does not +prove provider-side authorization. Verify the integration with an approved controlled live test. + -**Use Microsoft's assisted process for authorized testing.** The repository provides offline -runtime tests, while Microsoft supplies the approved EPP caller and test procedure. An ordinary -`az account get-access-token` result or a customer test app represents a different caller. -Keep `allowedApplications` and Easy Auth unchanged, and keep local hosts private. Ask the -onboarding owner to arrange the approved procedure. Activate after that validation is complete. -If the procedure is not available, record **not validated** and leave the policy inactive. - -Provide the operator with the endpoint URL, customer tenant ID, endpoint application client ID, -identifier URI, encryption key ID, and **public** certificate or thumbprint. Also provide the -selected provider, intended channels, and approved test window from the deployment inventory. -Agree on controlled recipients privately and get permission before any paid live send. - -| Order / owner | Check | Evidence to retain without sensitive payloads | -|---|---|---| -| Azure operator | Read back HTTPS/Easy Auth and verify every serving route/hostname/slot has the intended issuer, audience, caller allowlist, and no SendOtp exemption. | Configuration review with resource identity and UTC time. | -| Azure operator | Perform the missing-token negative check below. | HTTP status; a rejection is not a healthy evaluation. | -| Authorized test operator | Invalid/expired/signature/wrong-issuer/wrong-audience tokens and unauthorized callers fail before handler execution. Pair negative checks with valid evaluations to rule out a general outage. | Expected versus observed status, UTC time, safe request/correlation IDs. | -| Authorized test operator | Valid encrypted `mode: 2` / `"evaluation"` request returns `200` and the matching nonce. Tampered/invalid envelopes fail. | Nonce match **boolean**, status, latency, and correlated application events; never the nonce/body. | -| Authorized test operator + provider | Submit an approved `mode: 1` live test for each intended channel after evaluation succeeds. Verify provider acceptance and actual handset receipt/audio. | Acceptance status plus separate recipient/provider delivery confirmation. No phone number, OTP, or raw response. | -| Operations owner | Confirm request/log ingestion, correct runtime schema, and notifications. | Saved query/resource scope and tested action-group/alert routing; see [monitoring](MONITORING.md). | - -This bounded PowerShell check sends **no token and no OTP data**, and does not follow redirects. -Use the summary's exact HTTPS endpoint, never an example hostname or a provider URL: +## Validate the endpoint + +Use Microsoft's approved caller and test procedure. A customer test application or ordinary Azure +CLI token is a different caller. + +Provide the authorized operator: + +- endpoint URL +- customer tenant ID +- endpoint application client ID and identifier URI +- encryption key ID and public certificate or thumbprint +- selected provider and channels +- approved test window and controlled recipients + +| Order | Check | Safe evidence | +| --- | --- | --- | +| 1 | Read back HTTPS and Easy Auth issuer, audience, caller allowlist, and excluded paths | Resource ID, UTC time, and configuration review | +| 2 | Send a request without a token | Expected `401` on the default single-region configuration | +| 3 | Confirm invalid, expired, wrong-issuer, wrong-audience, and unauthorized callers fail | Status and safe request identifiers | +| 4 | Send a valid encrypted evaluation request | `200`, matching nonce confirmed locally, latency, and correlated events | +| 5 | Send one approved live SMS and voice request | Provider acceptance plus separate recipient confirmation | +| 6 | Confirm logs and notifications | Saved query, resource scope, and tested action group | + +The following check sends no token and no OTP: ```powershell $endpoint = 'https:///api/SendOtp' @@ -219,87 +194,70 @@ $response = Invoke-WebRequest -Uri $endpoint -Method Post -ContentType 'applicat $response.StatusCode ``` -On the default single-region Easy Auth configuration, expect `401`. A `403` may instead be a -network/access rejection and needs investigation; `200` or a handler validation `400` means -this did not demonstrate the expected authentication gate. Transport/redirect errors are not -passes. This only checks the missing-token case, not all authorization or readiness properties. +Expect `401` for the default Easy Auth configuration. Investigate `403`, redirects, transport +errors, `200`, or handler `400` results. This checks only the missing-token case. -Evaluation skips provider selection/credential lookup and provider HTTP **on its request path**. -With the selected cache enabled, configured workers can independently acquire credentials at startup; -JavaScript/Python also poll for refresh. Do not confuse those events with an evaluation sending a message. +Evaluation validates the authorized encrypted request path without sending a message. It does not +validate provider credentials, provider acceptance, or recipient delivery. For live requests, +`200` means provider acceptance. Confirm delivery separately and do not blindly retry timeouts. -For live requests, `200` with matching nonce means **provider acceptance, not delivery**. -Soprano voice extracts the first six-digit sequence; Telesign voice paces standalone six-digit -runs and repeats the message. Verify the actual locale, spoken digits, and recipient experience. -Do not blindly retry timeouts: provider acceptance can precede a lost response or caller fallback. Repeat deployed checks after package, authentication, credential, key, or routing changes. -## Operations lifecycle +## Operations -Only after the acceptance checks pass, have the policy administrator follow -[manual activation and rollback](../setup/docs/README.md#step-3---manually-validate-and-activate-policy). -Public Graph SMS/voice schemas do not document the EPP `url`/`appId` update. Obtain the -product-approved procedure; no guessed PATCH body is supplied here. +After validation, have the policy administrator use the +[approved activation and rollback process](../setup/docs/README.md#step-3---manually-validate-and-activate-policy). +Save the prior policy, preserve unrelated properties, and read back the result. -For handover, [find the linked monitoring resources](MONITORING.md#find-the-linked-resources-in-the-portal), -discover requests, then use [your runtime's event schema](MONITORING.md#runtime-specific-log-discovery). -There is no universal JSON request summary; operation, invocation, and application IDs differ. -Easy Auth can reject before application logs exist; `response_prepared` does not prove caller receipt. +Assign owners for: -Use [collection diagnostics](APPLICATION-INSIGHTS.md) for missing data and -[endpoint triage](../setup/docs/Troubleshooting.md#after-deployment-triage) for failures. -[Identity monitoring](SIGN-IN-MONITORING.md) is separate; setup does not export tenant logs or create alerts. +- request failures, latency, and telemetry gaps +- provider account access and credential rotation +- certificate renewal +- alerts and incident routing +- retention and cost +- policy rollback -Assign ongoing owners for request failures/latency, provider account and credential rotation, -certificate expiry/renewal, alert routing, telemetry gaps, retention/cost, and policy rollback. -Setup creates telemetry resources, **not** alert rules, action groups, availability tests, -certificate contacts, or provider delivery receipts. Complete those manual operations before -relying on alerts. Keep the saved summary, approved policy snapshots, validation evidence, -package/source version, and renewal dates in restricted operational storage. +Setup creates telemetry resources, not alerts, action groups, availability tests, certificate +contacts, or provider delivery receipts. Keep the deployment summary, policy snapshots, validation +evidence, package version, and renewal dates in restricted operational storage. -## Custom development and manual deployment - - -**Skip after guided setup unless customizing code.** Follow the -[JavaScript](../javascript/README.md), [Python](../python/README.md), or [.NET](../dotnet/README.md) -local setup. Copy [sample settings](local.settings.sample.json) beside that runtime's `host.json`; -replace placeholders, use string `Values`, and select `node`, `python`, or `dotnet-isolated`. - -`UseDevelopmentStorage=true` needs local Azurite. Use a local **test** PEM/base64 PEM for -`EPP_DECRYPTION_KEY_PEM`; Core Tools cannot resolve Azure Key Vault references. For evaluation-only -local work, omit provider settings so startup does not try to acquire managed-identity credentials. -Your CLI login cannot substitute for the runtime's managed identity. Bind the host to loopback, -without tunnels/public forwarding: Core Tools has no Easy Auth. Never copy the production -private key, telemetry credentials, or all cloud settings to a workstation. - -For custom cloud deployment, follow [packaging instructions](../TECHNICAL.md#download-a-function-zip) -and the selected plan's deployment mechanism. Python/.NET source ZIPs are not runnable -run-from-package artifacts. Exclude local settings, keys, tests, and diagnostic scripts using the -runtime's `.funcignore` and project publish rules. Source edits or a locally built ZIP do not -update Azure; apply approved cloud settings separately and rerun deployed acceptance checks. +## Custom deployment - -### Manual deployment trust checklist +Skip this section after guided setup unless you are changing code or deploying a custom package. + +For local work: + +- follow the selected [JavaScript](../javascript/README.md), [.NET](../dotnet/README.md), or + [Python](../python/README.md) guide +- use private test settings and a local test encryption key +- keep Core Tools on loopback because it does not provide Easy Auth +- omit provider settings for evaluation-only work +- never copy production keys or credentials to a workstation + +For cloud deployment, use the [package guidance](../TECHNICAL.md#download-a-function-zip). +Exclude local settings, keys, tests, and diagnostics. Apply cloud settings separately and repeat +the deployed validation checks. -Guided setup already configures these. A manual deployer must retain equivalent platform -protection **before opening ingress**; the anonymous handler has no backup token validator. + + +### Easy Auth checklist -| Easy Auth setting | Required value | -|---|---| +| Setting | Required value | +| --- | --- | | `globalValidation.requireAuthentication` | `true` | | `globalValidation.unauthenticatedClientAction` | `Return401` | | `httpSettings.requireHttps` | `true` | -| `identityProviders.azureActiveDirectory.registration.clientId` | Endpoint app client ID | -| `identityProviders.azureActiveDirectory.registration.openIdIssuer` | Trusted tenant issuer matching token version; never `common` or `organizations` | -| `identityProviders.azureActiveDirectory.validation.allowedAudiences` | Exact endpoint-app audience agreed with the authorized caller | -| `identityProviders.azureActiveDirectory.validation.defaultAuthorizationPolicy.allowedApplications` | Nonempty authorized Microsoft caller app-ID allowlist, not the endpoint app ID | -| `globalValidation.excludedPaths` | No SendOtp or alternate-ingress exemption | - -For v1, use the agreed Application ID URI audience and `https://sts.windows.net/{tenantId}/` -issuer; for v2, use the matching tenant-specific v2 issuer and agreed audience, normally the -endpoint app client-ID GUID. Never derive trust from the request's `tenantId` or forwarded headers. -Leave the app registration's `tokenEncryptionKeyId` **null**: encrypted Entra bearer tokens are -not supported. This does not disable JWE payload encryption. Never delete encryption credentials -or change signing keys just to make token validation pass. +| `identityProviders.azureActiveDirectory.registration.clientId` | Endpoint application client ID | +| `identityProviders.azureActiveDirectory.registration.openIdIssuer` | Trusted tenant-specific issuer | +| `identityProviders.azureActiveDirectory.validation.allowedAudiences` | Exact endpoint audience | +| `identityProviders.azureActiveDirectory.validation.defaultAuthorizationPolicy.allowedApplications` | Nonempty approved Microsoft caller app-ID allowlist | +| `globalValidation.excludedPaths` | No `SendOtp` or alternate-ingress exemption | + +The handler has no fallback token validator. Never use `common` or `organizations` as the issuer, +derive trust from request fields, or exempt `SendOtp`. + +Keep the app registration's `tokenEncryptionKeyId` null. Microsoft Entra bearer-token encryption +is separate from the JWE delivery payload and is not supported by this sample. diff --git a/docs/README.md b/docs/README.md index e614040..42300f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,18 +20,21 @@ Use the guide that matches your task. ## Understand and develop -- [Architecture, settings, and built-in providers](../TECHNICAL.md) +- [Technical reference](../TECHNICAL.md) + - [Soprano and Telesign](../TECHNICAL.md#built-in-providers) - [Send OTP API](CONTRACT.md) -- Runtime guides: [JavaScript](../javascript/README.md), [C#](../dotnet/README.md), and - [Python](../python/README.md) +- Runtime guides: + - [JavaScript](../javascript/README.md) + - [C#](../dotnet/README.md) + - [Python](../python/README.md) - [Add Twilio](BRING-YOUR-OWN-PROVIDER.md) -- [Application log fields](LOGGING.md) ## Operate -- [Function monitoring and alerts](MONITORING.md) -- [Application Insights collection](APPLICATION-INSIGHTS.md) -- [Microsoft Entra ID monitoring](SIGN-IN-MONITORING.md) +- [Monitor the endpoint](MONITORING.md) + - [Application Insights setup and data](APPLICATION-INSIGHTS.md) + - [Application log fields](LOGGING.md) + - [Microsoft Entra ID sign-ins](SIGN-IN-MONITORING.md) - [Certificate renewal](../setup/docs/README.md#encryption-certificate-lifecycle) - [Rollback and decommissioning](../setup/docs/README.md#rollback-and-decommissioning) diff --git a/docs/SIGN-IN-MONITORING.md b/docs/SIGN-IN-MONITORING.md index ed42887..c5eee73 100644 --- a/docs/SIGN-IN-MONITORING.md +++ b/docs/SIGN-IN-MONITORING.md @@ -2,529 +2,254 @@ [Documentation](README.md) | [Function monitoring](MONITORING.md) +Use this page to investigate sign-ins and create identity alerts. Function execution, provider +acceptance, and delivery are monitored separately. + | Task | Start here | | --- | --- | -| Investigate one sign-in without creating resources | [Choose tenant/access](#1-choose-your-tenant-and-access), then [find the sign-in](#2-find-one-sign-in) | -| Set up ongoing identity alerts | [Recommended monitors](#recommended-monitors), then [export](#3-export-logs-for-ongoing-monitoring) and follow Steps 4-6 | -| Diagnose missing records or quiet alerts | [Check data](#4-check-your-data-and-choose-a-query), [monitor ingestion](#monitor-ingestion-separately) | -| Adapt a query or add advanced alerts | [Advanced monitoring reference](#advanced-monitoring-reference) | - -The six-step walkthrough creates one user sign-in alert. Function and delivery monitoring are separate. +| Investigate one sign-in | [Find a sign-in](#find-a-sign-in) | +| Create ongoing alerts | [Set up monitoring](#set-up-monitoring) | +| Choose the first alerts | [Recommended monitors](#recommended-monitors) | +| Fix missing or quiet data | [Collection health](#collection-health) | +| Review fields and query design | [Query reference](#query-reference) | ## Recommended monitors -Choose the tenant, exact identity/resource IDs, and notification owners. Use exact IDs. +Choose exact tenant, application, service-principal, managed-identity, and resource IDs. Display +names can change and are not reliable alert filters. - ### Integration sign-in failures -1. **Watch:** application/service-principal and managed-identity authentication in - **AADServicePrincipalSignInLogs** and **AADManagedIdentitySignInLogs**. Filter to the - known app/service principal **and** intended resource in the source tenant. -2. **Notify:** when failures exceed the normal level or a new error recurs. Group by identity, - resource, and error; include the last successful sign-in. -3. **First checks:** review the error, identity, credentials, federation, permissions, expected - schedule, token caching, and [collection health](#monitor-ingestion-separately). +- **Tables:** `AADServicePrincipalSignInLogs` and `AADManagedIdentitySignInLogs` +- **Scope:** the known identity and intended resource in the source tenant +- **Notify:** repeated or unusual failures +- **Check first:** error code, last success, credentials, federation, permissions, expected + schedule, token caching, and collection health - ### Credential and permission changes -1. **Watch:** **AuditLogs** for the integration's application and service-principal objects. - Include credential additions/removals, application identifier or trust-setting updates, - service-principal changes, consent, and permission grants/removals. Have the maintainer - verify which [audit activities](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-audit-activities#core-directory) - actually appear for these operations. -2. **Notify:** on an unexpected **successful** change. Keep failed attempts separate and record - approved maintenance. -3. **First checks:** review the actor, target, time, approved change, and changed-property names. - Link to restricted logs instead of copying raw values. - - +- **Table:** `AuditLogs` +- **Scope:** the integration's application and service-principal object IDs +- **Notify:** unexpected successful credential, consent, permission, trust, or service-principal + changes +- **Check first:** actor, target, time, approved maintenance, result, and changed property names + ### Repeated user sign-in failures -1. **Watch:** **SigninLogs** for the customer-facing app and source tenant. Review - **AADNonInteractiveUserSignInLogs** separately for background token/session problems; - do not mix those records into the interactive failure count or rate. -2. **Notify:** when an error recurs or unresolved failures exceed the baseline. Check later - outcomes because consent prompts and expired sessions can recover. -3. **First checks:** review the overall error, recent app or policy changes, and recovery records. - Successful MFA steps do not make the overall sign-in successful. - -Select the [required categories](#choose-additional-export-categories), verify -[fields](#field-checks-for-the-recommended-monitors), and reuse the -[alert setup](#5-create-your-first-alert). Set thresholds from normal traffic. - -## 1. Choose your tenant and access - -1. Open the [Microsoft Entra admin center](https://entra.microsoft.com). In **Settings > - Directories + subscriptions**, select the tenant where your users sign in. It may differ - from the tenant hosting the Function. Note its **Tenant ID** from **Overview**. -2. Ask your tenant administrator for **Reports Reader**, the minimum role for viewing - activity logs. See Microsoft's [access requirements](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-access-activity-logs#prerequisites). - This role does not grant workspace access. -3. Check whether you have a **workforce tenant** or an **external tenant** for customer apps. - Use the [tenant comparison](https://learn.microsoft.com/en-us/entra/external-id/customers/concept-supported-features-customers) - if needed. External tenants use a different export path in Step 3. - -## 2. Find one sign-in - -1. Open **Entra ID > Monitoring & health > Sign-in logs**. Choose a date range and filter - to the application under investigation. Open a record authorized for inspection. -2. In **Basic info**, check the **Application**, **Application ID**, **Resource**, outcome, - and time. Keep the application's **client ID**, not its Object ID, for later query filters. - This may not be the endpoint `applicationId` in your EPP deployment summary. - The [activity-details guide](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-in-log-activity-details#basic-info) - explains these fields. Note the displayed time zone; use UTC when comparing other logs. -3. Open **Authentication Details** to see the methods, step outcomes, and policy context. - Look for an interrupted challenge, policy rejection, or a step satisfied by a previous - authentication. Microsoft's [Authentication Details explanation](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-in-log-activity-details#authentication-details) - helps interpret the result. - - ![Microsoft Entra ID sign-in Activity Details with Authentication Details selected and two Previously satisfied steps marked true](images/sign-in-monitoring/sign-in-activity-details-authentication.png) - - *Official Microsoft Entra ID example from - [Microsoft's activity-details instructions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-in-log-activity-details#authentication-details). - [Original screenshot](https://github.com/MicrosoftDocs/entra-docs/blob/927efa819a6f89a40b39b178d702b30f49b63f00/docs/identity/monitoring-health/media/concept-sign-in-log-activity-details/sign-in-activity-details-authentication.png), - Copyright (c) Microsoft Corporation. This is not your sign-in or a delivery test. - Previously satisfied does not prove a new phone challenge or provider send. - Your methods, policy results, timestamps, and portal layout can differ.* - -4. Use the error code and reason with Microsoft's - [sign-in troubleshooting guidance](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-troubleshoot-sign-in-errors). - If the issue points to the endpoint or provider, follow the - [Function monitoring guide](MONITORING.md#runtime-specific-log-discovery) separately. - -A single investigation can end after the portal review. Ongoing alerts, longer-term analysis, -and shared monitoring require the remaining steps. - -## 3. Export logs for ongoing monitoring - -1. Agree on a workspace, region, budget, and person responsible for the alerts. - For a **workforce tenant**, follow the - [integration prerequisites](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-integrate-activity-logs-with-azure-monitor-logs#prerequisites): - at least **Security Administrator** in Entra, an Azure subscription, a prepared workspace, - and suitable Azure access. If needed, your Azure administrator can - [create a workspace](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/quick-create-workspace). -2. For an **external tenant**, first follow the - [external-tenant setup instructions](https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-azure-monitor#prerequisites), - not the workforce setup above. They establish access to a workspace in a workforce - subscription. Return here once that access is ready; [extra setup detail](#prepare-the-external-tenant-access-path) - is available below. -3. In the **source tenant**, open **Entra ID > Monitoring & health > Diagnostic settings**. - Check existing settings before changing them. Select **Add diagnostic setting**, or - edit an existing setting with its maintainer's agreement. - - ![Microsoft Entra admin center showing Monitoring and health, Diagnostic settings, and Add diagnostic setting highlighted](images/sign-in-monitoring/diagnostic-settings-start.png) - - *Official Microsoft Entra admin center example from - [Microsoft's diagnostic-settings instructions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings#how-to-access-diagnostic-settings). - [Original screenshot](https://github.com/MicrosoftDocs/entra-docs/blob/927efa819a6f89a40b39b178d702b30f49b63f00/docs/identity/monitoring-health/media/howto-configure-diagnostic-settings/diagnostic-settings-start.png), - Copyright (c) Microsoft Corporation. This is not your tenant; menus and categories vary. - Follow the navigation above, not the example's breadcrumb. Visible risk categories do - not establish availability or licensing in an external tenant.* - -4. Name the setting and select **SignInLogs** for interactive user sign-ins. Under - **Destination details**, select **Send to Log Analytics workspace** and choose your - subscription and workspace. Select **Save**, reopen the setting, and check the destination. - The [illustrated export instructions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-integrate-activity-logs-with-azure-monitor-logs#send-logs-to-azure-monitor) - show these controls. -5. Allow time for records to arrive. Initial export is not immediate and does not backfill - all portal history. Microsoft describes about 45 minutes in its - [External ID monitoring repository](https://github.com/microsoft/external-id-azure-monitor) - and allows up to three days in the - [general setup guidance](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings#basic-process). - These are initial setup allowances, not an ongoing latency SLA. - -## 4. Check your data and choose a query - -1. Open **Azure portal > Log Analytics workspaces > your workspace > Logs** with an account - that has workspace query access, such as **Log Analytics Reader**. In **Tables**, find - **SigninLogs**, expand its columns, and preview recent records. The - [Logs tutorial](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#view-table-information) - shows the controls using `AppRequests`; choose **SigninLogs** in your own workspace. -2. Check `AADTenantId` and `AppId` against your source tenant and app client ID. - Compare the columns with the [SigninLogs schema](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs#columns). - No matching records is **not** a healthy result: check the destination, time range, - category, access, and portal sign-ins before continuing. -3. Open Microsoft's [Failed Signin reasons query](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/signinlogs#failed-signin-reasons). - In **KQL mode** (Kusto Query Language, used to search logs), copy it from that source - and filter to your `AADTenantId` and `AppId` - **before summarizing**. Ask someone familiar with KQL to review the edits. Check the - string type of `ResultType` and handle empty codes separately; these are authentication - codes, not provider HTTP statuses. [Query adaptation details](#4-establish-the-baseline) - explain the filters and other official query choices. -4. Choose an observation window with the receiving team, then **Run** that same calculation - across normal, peak, and quiet periods in a representative business cycle. Read `Count` - and the result codes, not the number of summary rows. Allow for measured ingestion delay; - use your baseline (the usual failure count over the chosen period) to choose a threshold - for that window, not a whole week's total. There is no - universal threshold; the source query is not a turnkey EPP or fraud detector. -5. Select **Save > Save as query** and choose the agreed query pack. Saving needs - **Log Analytics Contributor**; an authorized operator must make changes when access is read-only. - See [saving queries](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/save-query). - Keep the filters, count units, baseline, and chosen threshold with the monitoring setup notes. - -The scoped query can be reused for the first alert. Add rate, volume, and IP-based detection -only after establishing the [baseline](#more-alert-types). - -## 5. Create your first alert - -1. Check [alert permissions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule#prerequisites): - read access to the workspace, write access to the rule's resource group, and read access - to its action group (who gets notified). Creating groups or assigning roles requires additional permissions. - In **Azure portal > Monitor > Alerts > Create > Alert rule**, select your **Log Analytics - workspace** as **Scope**, not the Function or app registration. -2. On **Condition**, choose **Custom log search**, paste the saved query, and select **Run**. - Check **Preview**, then **Continue Editing Alert**. Follow the - [illustrated condition instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule#configure-alert-rule-conditions). - This is a scheduled log search alert, not an Azure Activity Log alert; check that your - table plan supports the query and rule. -3. For **Failed Signin reasons**, set **Measure** to **Calculation of a numeric column**, - choose `Count`, and use **Total** across the result-code groups. - Leave **Split by dimensions** unset for this first combined alert. Do not count summary - rows as failures. Set **Static**, **Greater than**, and a **Threshold value** based on - the normal counts for the same observation window in Step 4. -4. Set **Aggregation granularity (window size)** and **Frequency of evaluation** to match - the team's observation window, ingestion delay, response needs, and budget. Review - **Override query time range** with the query's time filter; alert scans are limited to - **48 hours**. Start with one evaluation above the threshold; multiple evaluations require review of the - [required query output and evaluation settings](#query-identity-and-rule-options) first. - Review healthy and qualifying periods in Preview. Keep missing-data expectations separate from failure counts; - [collection-health guidance](#monitor-ingestion-separately) explains why. -5. On **Actions**, select or create an **action group** for the receiving team. Use the - [action-group instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups#create-in-the-azure-portal) - to configure email/push or incident routing and complete recipient verification. - On **Details**, set the rule's name, description, severity, region, and resource group. -6. Choose the query **Identity**. For a **system-assigned managed identity**, clear - **Enable upon creation**, create the rule disabled, and have your Azure administrator - grant it reader/query access to the workspace. Wait for role propagation before enabling. - An interactive query working under the operator account does not prove the rule can read logs. - Review **Automatically resolve alerts** and [identity options](#query-identity-and-rule-options) - before **Review + create**. Keep the rule disabled until ready for the test below. +- **Tables:** `SigninLogs` for interactive sign-ins and + `AADNonInteractiveUserSignInLogs` for background token or session failures +- **Scope:** the customer-facing app and source tenant +- **Notify:** recurring errors or failures above the normal baseline +- **Check first:** overall result, recent app or policy changes, and later recovery records + +Keep the streams separate. A successful authentication step does not make the complete sign-in +successful. Set thresholds from representative traffic, not a universal sample value. + + +## Find a sign-in + +### 1. Choose the tenant + +1. Open the [Microsoft Entra admin center](https://entra.microsoft.com). +2. In **Settings > Directories + subscriptions**, select the tenant where users sign in. +3. Record its **Tenant ID** from **Overview**. +4. Ask for **Reports Reader** to view activity logs. Workspace access is separate. + +The sign-in tenant can differ from the tenant hosting the Function. External tenants use a +different export setup from workforce tenants. + + +### 2. Review the record + +1. Open **Entra ID > Monitoring & health > Sign-in logs**. +2. Filter by date and application, then open an authorized record. +3. In **Basic info**, check the application, application ID, resource, result, and time. + Keep the application's client ID for later filters. +4. In **Authentication Details**, review each method, step result, and policy context. + +![Microsoft Entra ID sign-in Activity Details with Authentication Details selected and two Previously satisfied steps marked true](images/sign-in-monitoring/sign-in-activity-details-authentication.png) + +*Official Microsoft Entra ID example from +[Microsoft's activity-details instructions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-in-log-activity-details#authentication-details). +[Original screenshot](https://github.com/MicrosoftDocs/entra-docs/blob/927efa819a6f89a40b39b178d702b30f49b63f00/docs/identity/monitoring-health/media/concept-sign-in-log-activity-details/sign-in-activity-details-authentication.png), +Copyright (c) Microsoft Corporation. This is not your sign-in or a delivery test. Portal layout +and results can differ.* + +`Previously satisfied` does not prove that a new phone challenge or provider send occurred. +Use the [sign-in error guide](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-troubleshoot-sign-in-errors) +for the overall result. For endpoint or provider issues, use +[Function monitoring](MONITORING.md#runtime-specific-log-discovery). + +## Set up monitoring + + +### 3. Export the required logs + +For a **workforce tenant**, prepare: + +- Security Administrator in Microsoft Entra ID +- an Azure subscription and Log Analytics workspace +- Azure access to configure the destination + +For an **external tenant**, follow the +[External ID Azure Monitor setup](https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-azure-monitor#prerequisites). +The workspace is hosted through a workforce tenant and can require Azure Lighthouse delegation. + +Then: + +1. In the source tenant, open **Entra ID > Monitoring & health > Diagnostic settings**. +2. Review existing settings with their owner. +3. Add or edit a setting and choose **Send to Log Analytics workspace**. +4. Select the categories needed for the monitors below. +5. Save, reopen the setting, and confirm the destination. + +![Microsoft Entra admin center showing Monitoring and health, Diagnostic settings, and Add diagnostic setting highlighted](images/sign-in-monitoring/diagnostic-settings-start.png) + +*Official Microsoft Entra admin center example from +[Microsoft's diagnostic-settings instructions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings#how-to-access-diagnostic-settings). +[Original screenshot](https://github.com/MicrosoftDocs/entra-docs/blob/927efa819a6f89a40b39b178d702b30f49b63f00/docs/identity/monitoring-health/media/howto-configure-diagnostic-settings/diagnostic-settings-start.png), +Copyright (c) Microsoft Corporation. This is not your tenant. Available categories and menus can +differ.* + +| Diagnostic category | Workspace table | +| --- | --- | +| `SignInLogs` | `SigninLogs` | +| `NonInteractiveUserSignInLogs` | `AADNonInteractiveUserSignInLogs` | +| `ServicePrincipalSignInLogs` | `AADServicePrincipalSignInLogs` | +| `ManagedIdentitySignInLogs` | `AADManagedIdentitySignInLogs` | +| `AuditLogs` | `AuditLogs` | + +Initial export is not immediate and does not backfill all portal history. Record the observed +arrival delay before setting alert windows. + + +### 4. Confirm the data + +1. Open **Log Analytics workspace > Logs**. +2. Find the selected table and preview recent records. +3. Check `AADTenantId` and `AppId` against the source tenant and application client ID. +4. Verify result types and IDs using the table schema. +5. Choose an official query from the + [Azure Monitor query collection](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/signinlogs). +6. Add tenant and application filters before aggregation. +7. Run the query across normal, peak, and quiet periods, then save it with its scope and baseline. + +No matching rows is not a healthy result. Check the destination, category, time range, access, +source activity, and ingestion delay. + +For the first alert, the official **Failed Signin reasons** query is a useful starting point. +`ResultType` is a string in `SigninLogs`. Treat empty results separately and do not confuse +authentication codes with provider HTTP status codes. + + +### 5. Create the alert + +1. In **Azure Monitor > Alerts > Create > Alert rule**, select the workspace as the scope. +2. Choose **Custom log search**, paste the saved query, and review **Preview**. +3. Match the alert measure to the query output. A summarized query needs its numeric column. +4. Choose the window, evaluation frequency, threshold, and missing-data behavior from the + measured baseline and ingestion delay. +5. Select or create an action group and verify every recipient. +6. Choose a query identity that can read the workspace. +7. Create the rule disabled when access must be granted after creation, then enable it after role + propagation and testing. + +Creating alerts requires workspace read access, rule write access, and access to the action group. +The operator's successful interactive query does not prove that the alert identity can read data. -## 6. Test and hand over - -1. Save the action group, open it, and select **Test**. Choose the notification receivers - and wait for the result; also check that the team receives the message. Microsoft's - [testing instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups#test-an-action-group-in-the-azure-portal) - cover failures and test permissions. This checks routing, not the sign-in query. -2. Test the query and a **temporary nonproduction rule** against authorized existing records. - Use a restricted test group and a temporary condition those records - satisfy. Check the query's window and portal override together. Verify the scheduled - alert and its later resolution, then remove the temporary rule/settings. See the - [full safe-test procedure](#validate-without-generating-provider-sends). -3. Do not generate live OTPs, repeated wrong codes, or production policy changes to populate - a chart. If records are insufficient, use an approved **non-phone test sign-in** in a - test tenant/app. An EPP evaluation does not guarantee a sign-in record or prove delivery. -4. Before enabling ongoing notifications, check that logs arrive, the rule identity has - access, the measurement matches your baseline, and the right people receive alerts. - Leave a teammate the query, rule, workspace, action group, settings, and support contacts. - Arrange a check for [missing logs](#monitor-ingestion-separately), not just sign-in failures. - -Enable the ongoing alert only after these checks pass. Advanced options follow below. +### 6. Test and hand over + +1. Test the action group and confirm the team receives the notification. +2. Test a temporary nonproduction rule against authorized existing records. +3. Verify firing and resolution, then remove temporary settings. +4. Do not generate live OTPs, repeated wrong codes, or production policy changes to create data. +5. Record the workspace, query, rule, identity, action group, thresholds, owner, and support route. +6. Add a separate collection-health check. + +Enable ongoing notifications only after these checks pass. -## Advanced monitoring reference - -For query maintainers: [access/licensing](#tenant-setup-access-and-licensing), -[external tenants](#prepare-the-external-tenant-access-path), [categories](#choose-additional-export-categories), -[schema](#inspect-schema-and-arrival-delays), [field checks](#field-checks-for-the-recommended-monitors), -[query adaptation](#adapt-official-queries), [rule options](#query-identity-and-rule-options), -[other alerts](#more-alert-types), [safe testing](#validate-without-generating-provider-sends), -[Workbooks](#build-a-workbook-when-you-need-a-shared-view), and [triage/costs](#investigate-alerts-and-manage-the-data). - - - -### Understand the evidence - -Counts are **stored records**, not unique customers, sessions, or sends. One authentication can -produce several records or succeed from an existing token claim without a phone challenge. -Non-success can mean an invalid password, interruption, cancellation, or policy rejection. -SMS/Voice/MFA labels do not identify the provider; `SigninLogs` guarantees no EPP mode, -provider-attribution, delivery, or cost field. - -Use [Function queries](MONITORING.md#2-single-region-function-queries) for execution, and provider -reports for delivery/billing. Evaluation and live requests can both return HTTP 200; neither proves receipt. - - - - -### Tenant access and licensing - -1. Microsoft Entra ID roles and Azure RBAC are separate. Use the roles in - [portal access](#1-choose-your-tenant-and-access) and [export setup](#3-export-logs-for-ongoing-monitoring). - Review - [workspace/table access](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/manage-access) - separately for queries. An external-tenant delegation does not grant every alert-management - or role-assignment operation in the subscription-owning workforce tenant. -2. Check current [monitoring licensing](https://learn.microsoft.com/en-us/entra/fundamentals/licensing#microsoft-entra-monitoring-and-health): - sign-in/audit logs are listed for Entra ID Free and P1/P2, but Graph APIs, premium reports, - risk features, and Workbook features can have other requirements. -3. The [external-tenant feature comparison](https://learn.microsoft.com/en-us/entra/external-id/customers/concept-supported-features-customers#general-feature-comparison) - currently lists **ID Protection as unavailable in external tenants**. Full supported - workforce [ID Protection capabilities](https://learn.microsoft.com/en-us/entra/id-protection/overview-identity-protection#license-requirements) - require P2 or an applicable entitlement. Do not buy workforce P2 expecting it to enable - external-tenant risk detections. - -### Prepare the external-tenant access path - -1. An external tenant cannot have an associated Azure subscription. Follow the - [illustrated wizard](https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-azure-monitor#set-up-the-azure-lighthouse-configuration-in-the-wizard) - for a workspace in a **workforce tenant** and Azure Lighthouse access. The prerequisites - include Azure RBAC **Owner** on the subscription and **Security Administrator or - Application Administrator** in the external tenant. Users configuring diagnostic settings - still need **Security Administrator**. -2. If already integrated, review the destination and access rather than repeating setup. - Otherwise select **Start set up** from external-tenant **Diagnostic settings**. - Sign in to the workforce tenant, select subscription/resource group, and specify workspace - name and region. In **Select user access**, choose the agreed users/groups and Azure roles. -3. Review the delegation, destination, region, permissions, and costs before **Create**. - Wait for confirmation, select **Done**, then configure export as in Step 3. - Do not move the EPP subscription or alter authentication policy to imitate workforce setup. - - - - -### Choose additional export categories - -1. Follow the [diagnostic-settings navigation](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings#how-to-access-diagnostic-settings) - and [category descriptions](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options) - when expanding the first export. Check categories available in your tenant and confirm - arrivals in each destination table. Selecting **SignInLogs** alone does not collect the - other sign-in streams or audit events. -2. Match each diagnostic category to its Log Analytics table: - **SignInLogs** to **SigninLogs**; **NonInteractiveUserSignInLogs** to - **AADNonInteractiveUserSignInLogs**; **ServicePrincipalSignInLogs** to - **AADServicePrincipalSignInLogs**; **ManagedIdentitySignInLogs** to - **AADManagedIdentitySignInLogs**; and **AuditLogs** to **AuditLogs**. - The recommended monitors need these streams where applicable. Keep their baselines separate. -3. **SignInLogs** does not enable **RiskyUsers** or **UserRiskEvents**. Provider OAuth logs - may be in the provider's tenant and need its administrator's help. Function diagnostic - settings and Application Insights are separate pipelines, not Entra export configuration. - - - -### Inspect schema and arrival delays - -1. Check the [workspace scope](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#open-log-analytics), - not a demo workspace or Application Insights `requests`/`traces` context. - Preview with [Simple mode](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-simple-mode#get-started-in-simple-mode) - and [expand a row](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#expand-and-sort-records). -2. Verify IDs and result types with the [field checks below](#field-checks-for-the-recommended-monitors). - In `SigninLogs`, `ResultType` is a **string**: zero is success, populated nonzero values are - authentication codes, and empty values remain unknown. -3. `CreatedDateTime` is sign-in initiation in UTC; `TimeGenerated` is the workspace timestamp, - not an ingestion-delay measurement. Adapt the - [ingestion-latency reference](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-ingestion-time) - and [standard-column timing guidance](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-standard-columns#_timereceived-column) - to your `SigninLogs` scope to measure delay, including missing timestamps. - These are not ready-made identity monitors or delivery-latency measures. - -### Field checks for the recommended monitors - -Use the [table preview](#4-check-your-data-and-choose-a-query) and verify real values before -saving an alert query. Portal JSON downloads are not Log Analytics rows. - -1. **Scope by the right IDs.** In sign-in tables, use **AADTenantId** for the exporting - directory and **AppId** for the client ID. For workload sign-ins, use - **ServicePrincipalId** for the identity's object ID; inspect **ResourceIdentity** and - **ResourceServicePrincipalId** in the - [service-principal schema](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs#columns) - and [managed-identity schema](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadmanagedidentitysigninlogs#columns). - Match the chosen column's ID type, not its display name. Standard **TenantId** denotes - the workspace; nullable portal home/resource tenant fields are not a substitute for - knowing the source directory. -2. **Check the overall result, per table.** Portal sign-in JSON uses integer - **status.errorCode** (zero for success) and **status.failureReason**. In Log Analytics, - inspect **ResultType**, **ResultSignature**, and **ResultDescription** against the - [interactive](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs#columns), - [noninteractive](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadnoninteractiveusersigninlogs#columns), - and workload schemas above. Some schemas describe Success/Failure while - [official query examples](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/aadserviceprincipalsigninlogs) - compare a zero code. Do not reuse one result test across all tables; verify values and - types in each destination, keeping empty/unknown results separate. -3. **Treat authentication steps as explanation.** Portal **authenticationDetails** is an - array; user-table **AuthenticationDetails** is a string requiring parsing. - **conditionalAccessStatus** or a step's **succeeded** value cannot replace the overall - result. An empty **authenticationMethodsUsed** summary does not establish absence of MFA, - and an MFA requirement or **Previously satisfied** step does not prove a fresh challenge. - Use the [step interpretation guidance](#inspect-authentication-steps-safely). -4. **Count records, not journeys.** Portal **id** / table **Id** identifies a record. - Keep streams distinct. **correlationId** / **CorrelationId** and **originalRequestId** / - **OriginalRequestId**, where populated, help investigate related outcomes, not establish - unique customers or guaranteed recovery. Confirm app, identity, and time context; - [correlation IDs are not guaranteed accurate](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-sign-in-log-activity-details#unique-identifiers). -5. **Inspect audit targets and results.** Use **AADTenantId**, **ActivityDisplayName**, - **ActivityDateTime**, **Result**, **ResultReason**, **InitiatedBy**, and **TargetResources** - from the [AuditLogs schema](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/auditlogs#columns). - Portal counterparts start lowercase. Within **targetResources**, match **id** and - **type** to the application or service-principal **object ID**, not the app's client ID. - Inspect **modifiedProperties** names in restricted logs. A successful result means - the operation completed, not that it was approved. **ActivityDateTime** is activity time; - **TimeGenerated** is record creation time. Verify source coverage rather than assuming - every trust/policy change or credential expiry appears here. - -### Inspect authentication steps safely - -1. Compare report entries with **Authentication Details** in the portal. Token claims or - previous authentication can satisfy a step; distinguish the step outcome from the whole sign-in. -2. Microsoft's [MFA Workbook template](https://github.com/microsoft/external-id-azure-monitor/blob/main/templates/mfaWorkbookDeployment.json) - currently queries **AuditLogs**, not `SigninLogs` authentication-step arrays. It needs - corresponding audit export/activity. Do not substitute tables or read it as a full step sequence. -3. A `SigninLogs` step report must handle `AuthenticationDetails` as the documented string - containing parsed array data, with empty/invalid/missing data and nullable outcomes. - Array expansion counts **steps**, not sign-ins. Missing steps are not success or failure. - Keep `authenticationMethodDetail` and raw payloads out of shared views; they can contain - phone identifiers and other personal data. - - - -### Adapt official queries - -1. In the [SigninLogs query collection](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/signinlogs), - **Failed Signin reasons** groups by code/reason; **Failed login Count** counts failures by - resource; **Signin Locations** groups success/failure by location. Select the required measure. - Some examples contain personal fields or limit MFA failures to one code; none is a - complete endpoint/provider-failure detector. -2. Add your source tenant/app filters **before aggregation**. Use the - [filtering tutorial](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#apply-multiple-query-filters) - or [Simple mode column filters](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-simple-mode#filter-by-column), - then switch to KQL to check them. Results-grid searches/header filters do not persist - as alert-query filters. Review numeric-looking code comparisons against the string schema. -3. Use the [time-range controls](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#set-the-query-time-range) - with any query time filter and verify the effective range. Preview **Show** limits and - browser-side grouping can hide data; do not treat them as the full baseline. -4. Save the reviewed query and share its location with a teammate who has log access. - Check that they get the same filters and columns. Distinct-user, record-count, and step - measures differ; note unknown-code handling and any exclusions alongside the query. - - - - -### Query identity and rule options - -1. Use the [scope screen](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule#configure-the-scope-of-the-alert-rule) - and [Details/Identity instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule#configure-alert-rule-details) - for the workspace rule. A **user-assigned identity** needs access before selection. - With **None**, query permissions follow the last editor. Review linked-storage policy - and identity restrictions before choosing. -2. Measurement depends on output: **Table rows / Total** suits unsummarized matching records; - a summarized query needs its numeric column. Sum and maximum of groups mean different things. - A greater-than-zero row condition is only suitable when the query returns **qualifying - incidents**, not an unconditional summary. -3. Review window, frequency, query time filter, and **Override query time range** together. - Overlapping windows can observe records repeatedly; late arrivals can be missed. - One-minute evaluation has query restrictions, including `ingestion_time()`. - Multiple violations require the documented datetime output/evaluation period. -4. **Split by dimensions** creates separate series/incidents and can raise costs/expose data. - Avoid high-cardinality IP/user/request/correlation identifiers without a specific need. - An unsplit workspace alert is a workspace incident; put app/scope in its description/runbook. -5. In a new action group, complete **Basics**, **Notifications**, and any **Actions**, then - **Review + create > Create**. Return to the rule and select the saved group. - Check processing region, recipients, required verification, and **Common alert schema** - where applicable. Keep security results restricted; do not attach automatic policy changes - to a heuristic. -6. **Automatically resolve alerts** makes the rule stateful; resolution timing depends on - frequency. Review **Mute actions** with the receiving team. Edit an existing rule through - **Monitor > Alerts > Alert rules > selected rule > Edit**. - -### More alert types - -These are optional additions after the first count alert is working. Each needs its own -reviewed query, baseline, and response plan. - -### A. Elevated non-success rate - -1. Use failure counts first if they answer the question. A rate requires failures and - total traffic for the **same tenant, app, population, and window**, with unknown-code - handling and a minimum traffic level. The official count query does not calculate this rate. -2. Check denominator coverage in healthy and empty windows before choosing a numeric rate - column/threshold. Document exclusions and monitor them separately. Route to authentication - support, not automatic provider-outage classification. - -### B. Unusual sign-in volume - -1. Review the [authentication Workbook template](https://github.com/microsoft/external-id-azure-monitor/blob/main/templates/authenticationsWorkbookDeployment.json) - for traffic queries, then adapt one to your scope. **User count per Application** measures - distinct users, not sign-in record volume. -2. Choose the limit from normal traffic and response capacity. Campaigns, releases, retries, - clients, and collection changes can explain spikes. This is not OTP-send volume, - SMS-pumping evidence, or provider spend. - -### C. Potential abuse from a source IP - -1. Use [Signin Locations](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/signinlogs#signin-locations) - and the [user-behavior template](https://github.com/microsoft/external-id-azure-monitor/blob/main/templates/userBehaviourWorkbookDeployment.json) - for investigation. Neither supplies a turnkey EPP fraud alert. -2. Have security review actual `IPAddress`, `UserId`, and `ResultType` population before - choosing a grouped rule. Account for unknown users, absent IPs, approximate distinct - counts, distributed attempts, NAT, VPNs, mobile networks, and shared proxies. - IP geolocation does not prove physical location. -3. Restrict personal-data results/notifications. Do not automatically block, disable an - account/provider, or change policy solely on these heuristics. -4. In supported, licensed workforce tenants, follow - [risk investigation instructions](https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-investigate-risk). - Check populated `RiskLevelDuringSignIn`, `RiskLevelAggregated`, `RiskState`, and `RiskDetail`. - Detections can arrive asynchronously; empty, none, or hidden is not fraud clearance. - A template named "risk detection" does not enable ID Protection in external tenants. - -### Monitor ingestion separately - -1. Assign someone to check collection as well as failures. See - [missing-data guidance](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-types#log-alerts): - quiet alerts can mean stopped export. An idle app and broken collection can look alike; - agree on normal traffic, quiet hours, and maintenance before enabling freshness paging. -2. Compare portal sign-ins with the workspace after the measured delay. Adapt the linked - ingestion guidance into a scoped check for **no rows, missing ingestion time, and stale - arrival**; test each case. It is not a preconfigured no-data alert. An unresolved table - or query error must remain visible, not become an empty-success fallback. -3. Create a separate rule with its own measure, threshold, lookback, frequency, recipient, - and maintainer. Match the query-time override to that calculation; do not blindly use - the aggregation window. Check the query's frequency restrictions. -4. Inspect rule health/query errors, export settings, workspace access, transformations, - ingestion limits, and service health. Fix evaluation failures separately from stale-data - incidents. Missing evidence does not mean less abuse. - -### Validate without generating provider sends - -1. Test the saved action group and confirm the team receives it. Fix **View details** failures - before testing the rule; read access alone may not grant test permission. -2. Run the saved query against healthy, qualifying, empty, and delayed windows. For a temporary - nonproduction rule, use existing records, a restricted action group, and an authorized rule identity. -3. Observe one scheduled alert and its resolution, then remove temporary overrides and restore - the intended settings. Keep the query time filter and portal override aligned. -4. Never lower production thresholds, trigger live OTPs, repeat wrong codes, bypass Easy Auth, - or change production policy for a monitoring test. Use an approved non-phone test sign-in if needed. - - - - -### Workbook - -1. Review the [Microsoft monitoring repository](https://github.com/microsoft/external-id-azure-monitor) - with the query maintainer. Its templates are deployable ARM resources, not text to paste into - Logs; inspecting them does not require selecting **Deploy to Azure**. -2. In **Monitor > Workbooks > New**, add the reviewed workspace query using Microsoft's - [Workbook authoring guide](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-create-workbook). - Verify tables, licensing, filters, units, time range, freshness, and personal-data exposure. -3. Save to the approved location and have another authorized user test access. Workbook sharing - does not grant log access, create alerts, or enable tenant risk features. - - - -### Investigate alerts and manage the data - -1. In **Monitor > Alerts**, verify the rule, workspace, tenant/app scope, UTC window, measured - value, and collection health. Re-run the saved query, then inspect the matching Entra details. -2. Separate user interruption and planned changes from outages or abuse. Escalate suspected - compromise through the security process; a heuristic is not a fraud verdict. -3. For phone concerns, compare [Function logs](MONITORING.md#runtime-specific-log-discovery) and - restricted provider records. Their correlation IDs are not guaranteed to match. Prove any - association; provider acceptance still is not handset receipt. -4. Keep identity, device, IP, and phone data in approved systems. Query filters are not access - control. Never put tokens, credentials, OTPs, messages, or raw personal data in alerts or issues. -5. Review [retention](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-configure), - [log costs](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs), caps, - transformations, alert frequency, and recipients with their owners. Keep the query, tests, - contacts, and escalation procedure together and walk a teammate through them. +## Query reference + +### IDs and results + +| Need | Use | +| --- | --- | +| Source tenant | `AADTenantId` | +| Application client | `AppId` | +| Service-principal object | `ServicePrincipalId` | +| Target resource | `ResourceIdentity` or `ResourceServicePrincipalId`, after checking the table schema | +| Overall result | `ResultType`, `ResultSignature`, and `ResultDescription` | +| Audit target | `TargetResources` object IDs and types | + +Do not use display names as the only scope. Standard workspace `TenantId` is not the source +directory ID. Portal field names and Log Analytics column names can differ. + +### Authentication steps + +`AuthenticationDetails` explains steps but does not replace the overall result. Previous token +claims can satisfy a step. Empty methods or missing step data do not prove that MFA was absent. + +If you parse step arrays, count steps separately from sign-ins. Keep phone identifiers and raw +authentication details out of shared dashboards and notifications. + +### Audit records + +Use `ActivityDisplayName`, `ActivityDateTime`, `Result`, `ResultReason`, `InitiatedBy`, and +`TargetResources`. Match target object IDs, not the application client ID. A successful audit +result means the change completed, not that it was approved. + +### Alert design + +- Add scope filters before aggregation. +- Keep interactive, noninteractive, service-principal, and managed-identity streams separate. +- Align query time filters, alert windows, and evaluation frequency. +- Use a minimum traffic count for rate alerts. +- Avoid high-cardinality dimensions such as user, IP, request, or correlation IDs unless required. +- Restrict personal-data results and notifications. +- Do not automatically block users, disable accounts, or change policy from a heuristic alert. + +Optional monitors such as unusual volume, source-IP patterns, and identity risk require a separate +baseline, response plan, privacy review, and applicable licensing. They are not provider delivery +or SMS-pumping signals. + + +## Collection health + +Quiet alerts can mean quiet traffic or stopped export. Monitor collection separately: + +1. Compare recent portal records with workspace records after the measured delay. +2. Check for no rows, stale arrival, query errors, and missing ingestion timestamps. +3. Review diagnostic settings, workspace access, transformations, limits, and service health. +4. Test the collection rule with healthy, empty, and delayed windows. +5. Route collection failures to an owner who can fix export or query access. + +Missing evidence does not mean fewer failures. + +## Privacy, licensing, and costs + +- Microsoft Entra roles and Azure RBAC are separate. +- External tenants have different subscription and export arrangements. +- Risk and premium report features can require additional licensing and may not be available in + external tenants. +- Sign-in records can contain personal data. Restrict workspace, alert, Workbook, export, and + incident access. +- Set retention and budget alerts deliberately. Cost controls do not replace collection-health + monitoring. + +Counts are stored records, not unique users, sessions, challenges, sends, or delivered messages. +Use [Function monitoring](MONITORING.md) for endpoint execution and provider reports for delivery +and billing. + +## References + +- [Access Microsoft Entra activity logs](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-access-activity-logs#prerequisites) +- [Export activity logs to Azure Monitor](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-integrate-activity-logs-with-azure-monitor-logs) +- [Diagnostic log categories](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options) +- [Sign-in logs table](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/signinlogs) +- [Service-principal sign-in table](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadserviceprincipalsigninlogs) +- [Managed-identity sign-in table](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/aadmanagedidentitysigninlogs) +- [Create a log search alert](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule) +- [Create and test an action group](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) +- [Microsoft Entra licensing](https://learn.microsoft.com/en-us/entra/fundamentals/licensing#microsoft-entra-monitoring-and-health) From a672f0f1173dc7ca388852a8d6c6d1d9bcfd53e1 Mon Sep 17 00:00:00 2001 From: Hou Chi Chan Date: Thu, 15 Oct 2026 16:22:51 -0700 Subject: [PATCH 4/5] docs: refine language and terminology Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe53050-0d2b-4d05-bcc1-20f5b611b665 --- README.md | 7 +-- TECHNICAL.md | 45 +++++++++++-------- docs/APPLICATION-INSIGHTS.md | 6 +-- docs/BRING-YOUR-OWN-PROVIDER.md | 2 +- docs/COST-CALCULATOR.md | 8 ++-- docs/FRONTDOOR.md | 62 +++++++++++++-------------- docs/GETTING-STARTED.md | 4 +- docs/LOGGING.md | 51 +++++++++++----------- docs/MONITORING.md | 13 +++--- docs/MULTI-PROVIDER-IMPLEMENTATION.md | 42 +++++++++--------- docs/ONBOARDING.md | 7 ++- docs/SIGN-IN-MONITORING.md | 8 ++-- dotnet/README.md | 21 +++++---- javascript/README.md | 19 ++++---- python/README.md | 21 +++++---- 15 files changed, 162 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 2db7b88..98f578c 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ Guided setup deploys **one regional endpoint** using JavaScript, C#, or Python. - JavaScript, C#, and Python implementations of one [HTTP and trust contract](docs/CONTRACT.md). - Guided Azure deployment with Easy Auth, Key Vault, and monitoring. -- No-send evaluation for authentication and decryption checks. +- Evaluation mode checks authentication and decryption without sending a message. - Built-in provider integrations and an [Add Twilio](docs/BRING-YOUR-OWN-PROVIDER.md) guide. ## Requirements -- Use a separate test environment with a Microsoft Entra tenant, Azure subscription, app +- Use a separate test environment with a Microsoft Entra ID tenant, Azure subscription, app registration, and unique resource name prefix. - Check [roles, permissions, tools, regional availability, and quota](setup/docs/README.md#prerequisites-for-step-2). - Plan for Azure, telemetry, and provider charges. The [cost estimate](docs/COST-CALCULATOR.md) is not a bill. @@ -37,7 +37,8 @@ Guided setup deploys **one regional endpoint** using JavaScript, C#, or Python. ![Microsoft Entra ID sends an encrypted request through Easy Auth to a Function, which uses Key Vault and calls the phone provider](docs/images/single-region-architecture.png) *Easy Auth checks the Microsoft caller before the Function decrypts the request. -Live success confirms provider acceptance. Confirm delivery separately. Evaluation sends nothing. +Live success confirms provider acceptance. Confirm delivery separately. Evaluation validates the +request without sending a message. See [architecture details](TECHNICAL.md#single-region-architecture). [Open the full-size diagram](docs/images/single-region-architecture.png).* diff --git a/TECHNICAL.md b/TECHNICAL.md index a36e28e..9d85448 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -4,13 +4,19 @@ [Documentation](docs/README.md) | [First deployment](docs/GETTING-STARTED.md) | [HTTP contract](docs/CONTRACT.md) -This reference covers implementation details, app settings, and packages. -Sections: [implementations](#implementations), [guided setup](#guided-epp-setup), -[download](#download-a-function-zip), [build](#build-zips-locally), -[HTTP details](#http-implementation-details), [settings](#configure-environment-variables), -[built-in providers](#built-in-providers), [production readiness](#production-readiness), -or [security](#security). For regional continuity, see -[resiliency and business continuity](#resiliency-and-business-continuity). +This reference covers implementation details, app settings, packages, and built-in providers. + +| Task | Section | +| --- | --- | +| Choose a runtime | [Implementations](#implementations) | +| Understand guided setup | [Guided EPP setup](#guided-epp-setup) | +| Download or build a package | [Download](#download-a-function-zip) or [build](#build-zips-locally) | +| Review request processing | [HTTP implementation details](#http-implementation-details) | +| Configure settings | [Environment variables](#configure-environment-variables) | +| Configure Soprano or Telesign | [Built-in providers](#built-in-providers) | +| Prepare for live use | [Production readiness](#production-readiness) | +| Review trust boundaries | [Security](#security) | +| Design regional continuity | [Resiliency and business continuity](#resiliency-and-business-continuity) | ## Single-region architecture @@ -117,7 +123,7 @@ pwsh -File ./package-javascript.ps1 -OutputPath ./artifacts/epp-javascript-v2.zi ## Request flow -Microsoft Entra ID → Easy Auth → anonymous HTTP handler (`POST /api/SendOtp`, validate envelope + decrypt JWE) → +Microsoft Entra ID → Easy Auth → Function 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 non-delivery path: after platform authentication, validate and decrypt, then echo the nonce without @@ -197,8 +203,8 @@ The following settings are for local or manual deployment. Start from [sample settings](docs/local.settings.sample.json). Every `Values` entry is a string. -Change `node` to `python` or `dotnet-isolated` as needed. Replace placeholders. The sample storage -value requires **Azurite** and must not be copied to Azure. +Set the runtime to `node`, `python`, or `dotnet-isolated`, then replace the placeholders. The +sample storage value requires **Azurite** and is for local use only. Local settings supply environment variables, **not a serialized `AppConfig` or request model**. For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript, @@ -229,7 +235,8 @@ The provider timeout defaults to `1500` and is capped at `2500` milliseconds. Each live request uses the endpoint for its selected channel. Endpoints must be absolute HTTPS URLs with a hostname, valid port, no user information, and no fragment. Redirects are not followed. -There is no shared endpoint fallback or automatic provider failover. +Each channel uses only its configured endpoint. Provider failover requires the +[advanced fallback design](docs/MULTI-PROVIDER-IMPLEMENTATION.md). Cache switches accept `true` or `false` and default to `true`. JavaScript and Python warm at startup and poll every 30 seconds. .NET warms once and refreshes on a cache miss. Disabling the selected @@ -253,12 +260,13 @@ deployment summary. Initial creation uses `phone-provider-encryption`. Replaceme 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). +the Function or update Microsoft Entra ID. 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 configured provider or authentication. `channel` selects the configured SMS or voice endpoint. -there is no deployment-level channel restriction. The legacy `EPP_PROVIDER_CHANNEL` setting is ignored. +Every deployment can serve both configured channels. The legacy `EPP_PROVIDER_CHANNEL` setting is ignored. ## Built-in providers @@ -284,7 +292,7 @@ The outbound managed identity first requests `api://AzureADTokenExchange/.defaul provider scope is used for the final token sent to `/messages/omnimsg`. The Function stores no Soprano client secret and does not forward the incoming Microsoft Entra ID token. Missing settings, blank tokens, and tokens with 30 seconds or less remaining fail before provider -HTTP. There is no API-key fallback or provider retry. +HTTP. Soprano uses OAuth only, and the adapter does not retry provider sends. For voice, the adapter extracts the first six consecutive digits from `message`, uses the request locale or `en-US`, and sends `beforePasswordText`, `password`, and `afterPasswordText` in @@ -328,20 +336,21 @@ Before live use: ## Security -**Easy Auth is the only caller-authentication gate before the anonymous Function.** +**Easy Auth is the caller-authentication gate before the Function runs.** Follow the [exact trust checklist](docs/ONBOARDING.md#manual-deployment-trust-checklist) for HTTPS, issuer, audience, caller allowlist, and unauthenticated rejection. Never disable it or exempt SendOtp. There is no handler token validator or function-key fallback. -JWE and its nonce prove no caller identity: anyone with the public key can encrypt. +JWE encryption and the nonce do not identify the caller. Anyone with the public key can encrypt. Provider credentials authenticate outbound calls, not incoming requests. Vault access uses managed identity. -Core Tools has no Easy Auth: bind locally to loopback without tunnels. +Because Core Tools does not provide Easy Auth, bind local hosts to loopback without tunnels. [Deployed checks](docs/ONBOARDING.md#validate-the-deployed-endpoint) remain required after offline tests. ## Docs Use the [documentation index](docs/README.md) for setup, troubleshooting, operations, and advanced designs. -For code work, keep the [HTTP contract](docs/CONTRACT.md) and [log schema](docs/LOGGING.md) together. +For code changes, review both the [HTTP contract](docs/CONTRACT.md) and +[log schema](docs/LOGGING.md). ## Contributing a language or provider diff --git a/docs/APPLICATION-INSIGHTS.md b/docs/APPLICATION-INSIGHTS.md index 699d959..a89a9a3 100644 --- a/docs/APPLICATION-INSIGHTS.md +++ b/docs/APPLICATION-INSIGHTS.md @@ -4,8 +4,8 @@ [Documentation](README.md) | [Monitoring](MONITORING.md) | [Log fields](LOGGING.md) -Use this page when telemetry is missing, incomplete, or unclear. For everyday queries and alerts, -start with [monitoring](MONITORING.md). +Start here when telemetry is missing or incomplete. For everyday queries and alerts, use +[monitoring](MONITORING.md). | Task | Go to | | --- | --- | @@ -16,7 +16,7 @@ start with [monitoring](MONITORING.md). | Diagnose missing data | [Coverage checks](#coverage-checks) | | Review privacy and sampling | [Limits](#limits) | -Function telemetry covers execution, timing, and logs. Microsoft Entra sign-ins and provider +Function telemetry covers execution, timing, and logs. Microsoft Entra ID sign-ins and provider delivery evidence are separate. diff --git a/docs/BRING-YOUR-OWN-PROVIDER.md b/docs/BRING-YOUR-OWN-PROVIDER.md index a104226..71459dd 100644 --- a/docs/BRING-YOUR-OWN-PROVIDER.md +++ b/docs/BRING-YOUR-OWN-PROVIDER.md @@ -5,7 +5,7 @@ [Documentation](README.md) | [Send OTP API](CONTRACT.md) | [Technical details](../TECHNICAL.md) -Use this guide to add a Twilio adapter to one runtime. +Add a Twilio adapter to one runtime. > [!NOTE] > Twilio is not included in the sample or configured by guided setup. Adding it requires code, diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index abc042a..842fb56 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -14,7 +14,7 @@ authentication steps by calling code. Apply provider rates to estimate a budget. | 3 | [Calculate the estimate](#convert-activity-into-a-cost-estimate) | | 4 | [Confirm provider rates](#confirm-provider-rates) | -The `authenticationDetails` data used by this guide is currently available through the +The report uses `authenticationDetails`, which is currently available through the [Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta). Review the beta API and script before each planning exercise. The script matches masked trailing digits to registered phone methods to derive the calling code. @@ -28,10 +28,10 @@ account must be allowed to consent to or use these Microsoft Graph delegated per - `UserAuthenticationMethod.Read.All` Reading other users' phone methods requires a -[supported Entra role](https://learn.microsoft.com/en-us/graph/api/authentication-list-phonemethods?view=graph-rest-1.0#permissions), -Authentication Administrator receives masked phone numbers; unresolved calling codes appear as `Unknown`. +[supported Microsoft Entra ID role](https://learn.microsoft.com/en-us/graph/api/authentication-list-phonemethods?view=graph-rest-1.0#permissions). +Authentication Administrators receive masked phone numbers. Unresolved calling codes appear as `Unknown`. -Access also depends on Microsoft Entra licensing, roles, and retention. The script exports +Access also depends on Microsoft Entra ID licensing, roles, and retention. The script exports aggregated calling codes, methods, and counts, not full phone numbers. Protect the output. Create `C:\temp` before running the script, or change the `Export-Csv` path to an diff --git a/docs/FRONTDOOR.md b/docs/FRONTDOOR.md index e5446ed..e959b45 100644 --- a/docs/FRONTDOOR.md +++ b/docs/FRONTDOOR.md @@ -53,7 +53,7 @@ priority, latency, and weight. Equal weights do not guarantee a 50/50 split. Start with two equal-priority regional stacks. Each region has its own Function App, capacity, storage, Key Vault, identities, telemetry, matching key, and provider credentials. Verify each can -operate without the other. Front Door, Microsoft Entra authentication, and the provider remain +operate without the other. Front Door, Microsoft Entra ID authentication, and the provider remain shared dependencies. ## 1. Plan the regional deployment @@ -72,8 +72,8 @@ Build and test the regional design outside live traffic. Review the caller application ID, certificate/public-key identity, selected provider route, and regional owners. - Plan rollout, validation, renewal, monitoring, drills, and rollback before activation. -Use the regional procedure in this guide rather than rerunning `Setup-Epp.ps1` against the same -application. The setup script creates one region; the steps below coordinate Front Door, shared +Follow this regional procedure rather than rerunning `Setup-Epp.ps1` against the same +application. The setup script creates one region. The steps below coordinate Front Door, shared key material, regional registrations, and failover without replacing an existing endpoint's application configuration or certificate unexpectedly. @@ -93,13 +93,13 @@ throttling. Verify hosting/subscription quotas, available warm capacity and scal storage and vault limits, identity/token-service capacity, and provider account/rate limits. Use measured load results in both failover directions. Autoscale limits, instance counts, and approved quotas are inputs, while sustained request rate, latency, errors, and saturation show -whether the design meets the agreed capacity target. This guide does not include a measured -production result. +whether the design meets the agreed capacity target. Validate the target deployment with its own +measured load result. Encrypted evaluation requests bypass request-path provider credential resolution and outbound provider calls. A double-load evaluation drill proves only the exercised ingress, authentication, decryption, and routing path, **not complete live-send capacity**. Background credential refresh -can still run independently; its presence does not prove live-path credential capacity. +can still run independently. Its presence does not prove live-path credential capacity. For full-path capacity evidence, test both survivor regions with a **non-delivering, provider-approved sandbox or stub**. Exercise the live request path, credential refresh, realistic @@ -124,12 +124,12 @@ Keep public access disabled while setting up authentication, keys, identities, a replaces the `Authorization` header. The original caller token must reach Easy Auth. Managed identity is still used by each Function for its own Azure dependencies. 4. Deploy the same package using private storage and managed-identity access. Keep SCM/basic - publishing authentication protections in place; do not open administrative endpoints to bypass + publishing authentication protections in place. Do not open administrative endpoints to bypass a deployment failure. 5. Configure the selected Security Store integration consistently in every region. Use each - region's own credential vault and identities; complete provider consent or credential setup as + region's own credential vault and identities. Complete provider consent or credential setup as required. If the integration uses managed-identity federation, configure trust for each regional - outbound identity on the authorized client application; do not reuse another region's identity ID. + outbound identity on the authorized client application. Do not reuse another region's identity ID. Do not forward the incoming caller token to the provider. 6. Configure regional Application Insights/Log Analytics and verify ingestion. Avoid making a serving origin depend on another region's credential vault or package storage. @@ -143,28 +143,28 @@ certificate is separate from this JWE encryption certificate. - Use an approved secure replication process. The test used Key Vault certificate backup/restore, which transfers an Azure-encrypted backup rather than a plaintext private key. - Key Vault backup/restore is restricted to the **same subscription and Azure geography**. Do not - assume it works across arbitrary subscriptions or geographies; obtain an approved alternative + assume it works across arbitrary subscriptions or geographies. Obtain an approved alternative design if these constraints do not fit. - Give each Function identity scoped access to its regional vault. Set `EPP_DECRYPTION_KEY_PEM` to the regional certificate's **versioned PEM backing-secret reference**. - Verify that the public keys match and each region can decrypt a request. Matching vault names or secret-version strings alone does not prove this. - Restored certificates are independent copies. They do not stay in sync automatically. Coordinate renewal - and Entra updates across every origin using the [certificate lifecycle guidance](../setup/docs/README.md#encryption-certificate-lifecycle). - That section explains the single-key constraints; apply the multi-region updates through an + and Microsoft Entra ID updates across every origin using the [certificate lifecycle guidance](../setup/docs/README.md#encryption-certificate-lifecycle). + That section explains the single-key constraints. Apply the multi-region updates through an approved manual procedure, not independent setup reruns. The sample has one active decryption - key; it does not implement overlapping multi-key rotation. + key and does not implement overlapping multi-key rotation. - Keep plaintext keys out of source, logs, tickets, and pipeline artifacts. Protect and remove temporary encrypted backups under the approved handling policy. ## 3. Provide a non-delivering readiness endpoint -Front Door's health probes do not carry the Entra caller token. Probing the protected `SendOtp` +Front Door's health probes do not carry the Microsoft Entra ID caller token. Probing the protected `SendOtp` endpoint would test authentication failure rather than application readiness. Implement a separate route such as **`/api/health/ready`** with this contract: -- Support HTTPS `HEAD` and `GET`; return no response body for `HEAD`. +- Support HTTPS `HEAD` and `GET`. Return no response body for `HEAD`. - Return `200` only when the handler is running and the configured RSA decryption key is usable. Return a generic `503` when not ready. - Do not send an OTP or call a phone provider. Do not return a nonce, keys, credentials, or detailed @@ -197,7 +197,7 @@ These are the **tested starting settings**. Validate them against the required p | Probe interval | 30 seconds | | Sample size / required successes | 4 / 3 | | Additional latency | 50 milliseconds | -| Origin response timeout | 30 seconds; this is not the caller's OTP deadline | +| Origin response timeout | 30 seconds. This is not the caller's OTP deadline. | | Route paths | `/api/SendOtp` and the implemented readiness path | | Accepted/forwarding protocol | HTTPS only | | Link to default domain | Enabled | @@ -219,7 +219,7 @@ Before allowing Front Door to reach the Functions, configure their access restri 1. Allow the **`AzureFrontDoor.Backend` service tag** with an additional **`X-Azure-FDID`** header condition matching this profile's Front Door ID. 2. Set unmatched requests to **Deny**. Both the source restriction and header condition are - required; the header alone can be spoofed. + required because the header alone can be spoofed. 3. Keep Easy Auth enabled for SendOtp. Restricting the network path does not authenticate the caller. 4. Open only the access needed by this public-origin design after the configuration is verified. A publicly resolvable hostname does not mean direct requests should be allowed. @@ -258,20 +258,20 @@ Approve synthetic data, bounded load, and the [drill's abort/restoration safegua in isolated nonproduction. Agree load/headroom, recovery, errors/latency, soak, and restoration deadlines first. Use an authorized harness, **not a real Microsoft Entra ID sign-in flow**. Evaluation suppresses -sends only inside this endpoint; caller fallback/retries can still deliver. Confirm caller behavior +sends only inside this endpoint. Caller fallback or retries can still deliver. Confirm 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; no nonce values or 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; successful surrounding evaluations. | +| 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. | | Failover from A to B | Follow the fire-drill: isolate only approved test origin A while leaving it **enabled in Front Door**. Keep B healthy and evaluation traffic bounded. | Health-based rerouting 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. | -| Failover from B to A | After A is fully restored and stable, repeat with B isolated but **enabled in Front Door**. | A meets the same agreed criteria; a one-direction pass is insufficient. | The same evidence, identifying A as the survivor and confirming B's subsequent restoration. | +| Failover from B to A | After A is fully restored and stable, repeat with B isolated but **enabled in Front Door**. | A meets the same agreed criteria. Both directions must pass. | The same evidence, identifying A as the survivor and confirming B's subsequent restoration. | | Sustained survivor capacity | In each direction, maintain the [combined peak offered load plus planned headroom](#capacity-gate-each-region-must-support-the-combined-peak-load) for the agreed soak. | **Each region handles at least double its normal share in the balanced two-region case, plus headroom**, within agreed error/latency and dependency limits. Evaluation proves only the exercised path. | Offered/completed rates, concurrency, latency percentiles, errors/timeouts, saturation/throttling, quotas and available capacity per region. | -| Restart, cold start, and credential refresh without the failed region | In a separately approved dependency-isolation test environment, make the failed region's storage/vault paths unavailable and start or restart the survivor stack. Exercise key resolution and credential refresh using the non-delivering provider setup below. Do not add a second fault to the basic fire-drill. | The survivor starts and serves using its own dependencies; warm caches do not hide cross-region access. Missing failed-region telemetry is not treated as success. | Dependency configuration/access evidence, startup/key/credential-refresh results, client outcomes and timestamps captured outside the failed region. | -| Complete live-path capacity and provider credential failures | Separately use a provider-approved, production-representative **non-delivering sandbox/stub** exercising credential acquisition/refresh and provider latency/quota behavior. Test valid and invalid/expired test credentials without production accounts or recipients. | Both survivor regions meet the capacity criteria on this path; credential failures are surfaced, not reported as successful sends. Evaluation-only throughput cannot pass this test. | Credential/provider timing, failures/throttling, load results, and provider capacity/quota evidence for behavior the sandbox/stub cannot reproduce. | +| Restart, cold start, and credential refresh without the failed region | In a separately approved dependency-isolation test environment, make the failed region's storage/vault paths unavailable and start or restart the survivor stack. Exercise key resolution and credential refresh using the non-delivering provider setup below. Do not add a second fault to the basic fire-drill. | The survivor starts and serves using its own dependencies. Warm caches do not hide cross-region access. Missing failed-region telemetry is not treated as success. | Dependency configuration/access evidence, startup/key/credential-refresh results, client outcomes, and timestamps captured outside the failed region. | +| Complete live-path capacity and provider credential failures | Separately use a provider-approved, production-representative **non-delivering sandbox/stub** exercising credential acquisition/refresh and provider latency/quota behavior. Test valid and invalid/expired test credentials without production accounts or recipients. | Both survivor regions meet the capacity criteria on this path. Credential failures are surfaced, not reported as successful sends. Evaluation-only throughput cannot pass this test. | Credential/provider timing, failures/throttling, load results, and provider capacity/quota evidence for behavior the sandbox/stub cannot reproduce. | | Restoration and conservative failback | Restore the original approved state even after a failed test. Verify the restored origin's own readiness and successful evaluations, then observe traffic returning for the agreed window. | Both origins are healthy and serving within agreed limits, with original security settings intact, before any next fault. | Restoration confirmation, origin-specific readiness/request evidence and failback latency/errors. | -| Interrupted-controller recovery | Verify the independent restoration timer or backup operator before fault injection. In an approved drill, interrupt the controller while the independent safeguard remains available. | The fault is removed by the agreed deadline without relying on the interrupted controller; unsafe load stops and unresolved recovery is escalated. | Interruption/restoration timestamps, safeguard or backup-operator actions, and both origins' final state. | +| Interrupted-controller recovery | Verify the independent restoration timer or backup operator before fault injection. In an approved drill, interrupt the controller while the independent safeguard remains available. | The fault is removed by the agreed deadline without relying on the interrupted controller. Unsafe load stops and unresolved recovery is escalated. | Interruption/restoration timestamps, safeguard or backup-operator actions, and both origins' final state. | For every row, record **pass, fail, or not tested**, the approved target, observed outcome, and evidence location. Assign an owner and retest date to gaps. A successful routing test is useful, @@ -286,7 +286,7 @@ instead of treating the observed timings below as an SLA. 1. **Approve the boundary.** Name the drill, change, telemetry, and restoration owners plus a backup. Record the exact test resources, window, isolation deadline, and restoration steps. - Confirm no production policy or real users depend on them; isolate only one origin at a time. + Confirm no production policy or real users depend on them. Isolate only one origin at a time. 2. **Prepare safe traffic and recovery.** Use an authorized caller with bounded encrypted `mode: 2` evaluation traffic—never live sends or real recipient data. Set rate, concurrency, timeout, duration, and cost limits. Keep the controller/evidence outside the target region and @@ -297,11 +297,11 @@ instead of treating the observed timings below as an SLA. and abort thresholds before injecting a fault. 4. **Isolate one origin without disabling it in Front Door.** Apply an approved reversible fault, such as stopping the test Function App, and timestamp request and confirmation separately. - Keep the survivor unchanged. Do not weaken Easy Auth or ingress; probe health is not a security boundary. + Keep the survivor unchanged. Do not weaken Easy Auth or ingress. Probe health is not a security boundary. 5. **Measure detection and survivor behavior.** Maintain the bounded combined-peak evaluation load plus headroom. Capture every attempt, failure, timeout, latency, safe correlation ID, probe, routing, and regional dependency signal. Missing telemetry is not success. Meet the full soak - criteria; evaluation-path results do not replace the separate live-path capacity evidence. + criteria. Evaluation-path results do not replace the separate live-path capacity evidence. 6. **Restore on completion or abort.** Stop unsafe load and restore the saved state at the deadline, any abort threshold, or completion. Cleanup must run even if assertions or the controller fail. Escalate immediately if restoration cannot be confirmed. @@ -322,7 +322,7 @@ and telemetry. Capture evidence outside the affected region. Include cold startu refresh, and restoration so the plan covers more than an already-warm app. Record any scenario that cannot be tested safely and assign an owner and follow-up date. -Use separate approved checks for real Microsoft Entra caller timing, provider acceptance, and +Use separate approved checks for real Microsoft Entra ID caller timing, provider acceptance, and handset delivery. Define retry and availability targets with the provider and onboarding owner. Front Door and this runbook do not guarantee zero downtime or make uncertain retries safe. @@ -354,7 +354,7 @@ The test controller was interrupted during one three-origin trial, extending tha recovered it. To keep the comparison fair, the table uses only the first eight minutes of each outage. We saw no failed samples before the outages or after restarting the Functions. -A failed request is not guaranteed to be replayed on another origin. The actual Entra caller's +A failed request is not guaranteed to be replayed on another origin. The actual Microsoft Entra ID caller's handling of these errors was not tested. These trials did not show a consistent benefit from 10-second probes or a third region. @@ -373,7 +373,7 @@ or successful delivery to a phone. reference resolution, certificate expiry, and provider outcomes. Never log OTP bodies, private keys, bearer tokens, or nonce values. - Roll out package/configuration changes deliberately and validate each region. Restore failed test - origins and verify they are serving; `Running` alone does not prove readiness. + origins and verify they are serving. `Running` alone does not prove readiness. - Keep the reviewed pre-change policy, origin access restrictions, authentication settings, and certificate-version references. If rolling back to a direct Function URL, first restore its approved single-region access policy **while retaining Easy Auth**, otherwise Front Door-only @@ -381,7 +381,7 @@ or successful delivery to a phone. - Have the policy administrator restore the reviewed URL/app ID through the supported procedure, read it back, and validate delivery. Do not delete resources as a substitute for policy rollback. - Retire extra resources only after traffic and policy rollback are confirmed. Front Door, regional - plans, and telemetry remain billable until appropriately decommissioned; respect Key Vault purge + plans, and telemetry remain billable until appropriately decommissioned. Respect Key Vault purge protection and retention. ## References diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index f9901ae..0ee1360 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -157,7 +157,7 @@ Complete the [approved endpoint checks](ONBOARDING.md#validate-the-deployed-endp both provider acceptance and receipt of the SMS or voice call. Acceptance alone is not proof of delivery. 4. Check that the request and relevant logs appear in Application Insights. -Use Microsoft's approved caller and test procedure for these checks; an ordinary Azure CLI token +Use Microsoft's approved caller and test procedure for these checks. An ordinary Azure CLI token cannot represent that caller. Activate the endpoint after every required check passes. If a live send times out, investigate its outcome before trying again because the provider may already have accepted it. @@ -170,7 +170,7 @@ They save the existing policy, apply the approved endpoint URL and application c preserve other settings, and validate the result. Repeat for each authentication method you intend to enable. SMS and voice can share the same -Function endpoint URL and application client ID; no additional setup run is needed for the second channel. +Function endpoint URL and application client ID. No additional setup run is needed for the second channel. Before handing over, confirm: diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 6ad0bf2..b9f4525 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -2,7 +2,7 @@ [Documentation](README.md) | [Find deployed logs](MONITORING.md#runtime-specific-log-discovery) | [HTTP contract](CONTRACT.md) -Use this page to identify safe event fields and runtime differences. +This reference defines safe event fields, request flow, and runtime differences. | Need | Section | | --- | --- | @@ -19,8 +19,8 @@ responses.** Keep body tracing off. | Runtime | Emission and context | | --- | --- | | JavaScript | JSON service records with immutable request context and existing camelCase/`x-ms-*` fields. | -| Python | Standard `logging`; `otp_log.py` defines fixed event IDs, names, levels, and fields. An immutable `LoggerAdapter` supplies Function and Microsoft trace identifiers. | -| .NET | Standard `ILogger`; `OtpLog` defines source-generated IDs/names. `ILogger.BeginScope` supplies `FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`, `MsCorrelationId`, and `MsCorrelationIdSource`. | +| Python | Standard `logging`. `otp_log.py` defines fixed event IDs, names, levels, and fields. An immutable `LoggerAdapter` supplies Function and Microsoft trace identifiers. | +| .NET | Standard `ILogger`. `OtpLog` defines source-generated IDs/names. `ILogger.BeginScope` supplies `FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`, `MsCorrelationId`, and `MsCorrelationIdSource`. | Each invocation ends with `request_completed`. Exporters control formatting and property preservation. Field names differ by runtime, so verify the @@ -32,21 +32,21 @@ 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; no raw body or arbitrary headers. | +| `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; no plaintext fields, JWE or key ID. | +| `delivery_context_decrypted` | Decryption completed. Excludes plaintext fields, JWE, and key ID. | | `provider_selected` | Registered provider and its authentication mode. | | `provider_credential_resolution_started` | OAuth client-assertion or Key Vault credential source, with explicitly named raw OAuth application/identity/tenant IDs. | -| `provider_credential_resolved` | Credential resolution and usability checks completed, with elapsed time; no secret, token, assertion or claims. | +| `provider_credential_resolved` | Credential resolution and usability checks completed, with elapsed time. Excludes secrets, tokens, assertions, and claims. | | `provider_request_build_started` | The provider is preparing the outbound request. | -| `provider_request_built` | Allowlisted HTTP method, final endpoint scheme/host/port/API path, HTTPS and disabled redirects; no query string, authorization headers or body. | +| `provider_request_built` | Allowlisted HTTP method, final endpoint scheme/host/port/API path, HTTPS, and disabled redirects. Excludes the query string, authorization headers, and body. | | `provider_request_started` | The outbound send is beginning, with method, sanitized endpoint and timeout. | -| `provider_response_received` | Actual upstream HTTP status; emitted before response-body reading completes. | +| `provider_response_received` | Actual upstream HTTP status. Emitted before response-body reading completes. | | `provider_response_processed` | Mapped provider status/outcome, fixed failure classification and duration. Raw provider descriptions and bodies are excluded. | | `response_prepared` | Response status and booleans indicating nonce/correlation inclusion, not their values or the response body. | -| `request_completed` | Final HTTP status, result, and elapsed time; stage-specific fields stay on their events. | +| `request_completed` | Final HTTP status, result, and elapsed time. Stage-specific fields stay on their events. | -JavaScript uses `envelope_validated`; Python/.NET use `payload_validated`. +JavaScript uses `envelope_validated`. Python and .NET use `payload_validated`. Evaluation skips provider work and emits `evaluation_completed`, `response_prepared`, and `request_completed`. @@ -55,15 +55,15 @@ Failures emit `request_failed` with stage, reason, and HTTP status (`FailureStag `Outcome` and a safe classification: `provider_http_error`, `provider_rejected`, `unrecognized_provider_status`, `missing_provider_message_id`, or `invalid_provider_json`. `provider_response_processed` includes that classification, upstream HTTP status, and normalized status, never descriptions/bodies. -Credential acquisition/refresh failures use `credential_refresh_failed`; unexpected failures use +Credential acquisition and refresh failures use `credential_refresh_failed`. Unexpected failures use `unexpected_error`. No event adds credential reads, acquisition, or send retries. -`response_prepared` runs before successful and failed returns; it does not prove caller receipt. +`response_prepared` runs before successful and failed returns. It does not prove caller receipt. `providerHttpStatus` is recorded when response headers arrive. An upstream `200` can precede a body-read timeout: `provider_response_received` may be followed by `request_failed` with Function `504`, without mapped provider status or success acknowledgement. -Unknown status text and malformed JSON are never logged; malformed JSON emits only +Unknown status text and malformed JSON are never logged. Malformed JSON emits only `provider_response_invalid_json` before provider outcome rules run. ## Safe fields and identifiers @@ -80,25 +80,26 @@ SDK diagnostics remain suppressed. query strings, and fragments, but not path segments. Keep secrets and personal data out of URL paths. Logging does not change the request. -JavaScript/Python events carry Function request/invocation IDs and Microsoft trace IDs available -at that stage. JavaScript also carries known channel/evaluation/provider context; Python uses -its adapter and event arguments. Initially only header IDs are available; a valid envelope can -later supply the selected correlation. The generated Function request ID joins events even when +JavaScript and Python events carry Function request and invocation IDs plus Microsoft trace IDs +available at that stage. JavaScript also carries known channel, evaluation, and provider context. +Python derives these fields from its adapter and event arguments. Initially, only header IDs are +available. A valid envelope can later supply the selected correlation. The generated Function +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. | +| `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 fallback 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`; disambiguates the selected value when the envelope and header differ. | +| `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`. | -| `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. | +| `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. | +| `providerEndpoint` | Validated final outbound URL without userinfo, query string, or fragment. Scheme, host/port, and API path remain visible. | External IDs keep their original value and case. Logs accept only 1-128 ASCII characters, starting with a letter or digit and followed by letters, digits, `.`, `_`, `:`, or `-`. Missing or invalid @@ -107,12 +108,12 @@ change logs only, not request behavior or trust. ## Levels and collection -Normal/completion events use **Information**; invalid requests, non-success 4xx outcomes, and -advisories use **Warning**; 5xx failures/timeouts use **Error**. +Normal and completion events use **Information**. Invalid requests, non-success 4xx outcomes, and +advisories use **Warning**. 5xx failures and timeouts use **Error**. Keep Information logs enabled for investigation without enabling SDK/body tracing. -Filters and sampling can remove events. Application events are logs, not host Request telemetry; -excluding `Request` from sampling does not retain every event. Configure collection and retention +Filters and sampling can remove events. Application events are logs, not host Request telemetry. +Excluding `Request` from sampling does not retain every event. Configure collection and retention deliberately. Easy Auth rejection happens before the handler: inspect platform/authentication diagnostics and caller observations rather than expecting an application event. diff --git a/docs/MONITORING.md b/docs/MONITORING.md index 47501a7..6a217fa 100644 --- a/docs/MONITORING.md +++ b/docs/MONITORING.md @@ -4,7 +4,7 @@ [Documentation](README.md) | [Application Insights](APPLICATION-INSIGHTS.md) | [Log fields](LOGGING.md) | [Troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage) -Guided setup creates Application Insights and Log Analytics. Use this page to confirm telemetry, +Guided setup creates Application Insights and Log Analytics. Start here to confirm telemetry, review failures and latency, and create alerts. | Task | Start here | @@ -16,7 +16,7 @@ review failures and latency, and create alerts. | Monitor several regions | [Multiple regions](#multiple-regions) | | Create notifications | [Alerts](#alerts) | | Diagnose missing data | [Collection and data quality](#collection-and-data-quality) | -| Monitor Microsoft Entra sign-ins | [Sign-in monitoring](SIGN-IN-MONITORING.md) | +| Monitor Microsoft Entra ID sign-ins | [Sign-in monitoring](SIGN-IN-MONITORING.md) | Run queries only after an [authorized test](ONBOARDING.md#validate-the-deployed-endpoint). Keep phone numbers, OTPs, request bodies, credentials, tokens, nonces, and raw provider responses @@ -167,7 +167,8 @@ AppTraces ``` Credential refresh can run outside a request. A successful evaluation does not prove provider -credentials work, and no warning does not prove they are healthy when collection or refresh is off. +credentials work. When collection or refresh is off, verify credential health directly instead +of relying on warnings. ## Runtime log formats @@ -280,8 +281,8 @@ Tune all sample thresholds to measured traffic and costs. Assign owners for certificate renewal, provider credentials, alert routing, telemetry retention, and cost. Follow the [certificate lifecycle runbook](../setup/docs/README.md#encryption-certificate-lifecycle). -A new Key Vault certificate version does not update the Function's pinned reference or Microsoft -Entra ID registration automatically. +A new Key Vault certificate version does not update the Function's pinned reference or the +Microsoft Entra ID registration automatically. Guided setup does not create action groups, alerts, Workbooks, availability tests, or certificate contacts. Cost budgets notify but do not stop spending. @@ -334,7 +335,7 @@ access restrictions and Easy Auth enabled while investigating routing. ingestion delay, filters, sampling, caps, retention, and exporter configuration. - **Sampling changes counts.** Use `ItemCount` where available. Weighted totals remain estimates. - **Pipelines differ.** Host requests, worker logs, resource metrics, diagnostics, and Microsoft - Entra sign-ins are separate data sources. + Entra ID sign-ins are separate data sources. - **Protect customer data.** Do not collect phone numbers, OTPs, bodies, encrypted payloads, tokens, credentials, private keys, nonces, or raw provider responses. - **Test the monitor.** Validate the query, rule identity, action group, notification, resolution, diff --git a/docs/MULTI-PROVIDER-IMPLEMENTATION.md b/docs/MULTI-PROVIDER-IMPLEMENTATION.md index 605b27e..176f939 100644 --- a/docs/MULTI-PROVIDER-IMPLEMENTATION.md +++ b/docs/MULTI-PROVIDER-IMPLEMENTATION.md @@ -91,25 +91,25 @@ references, and a valid deadline policy. For each selected [adapter](../javascript/src/functions/providers), inspect `credentialSpec` for the required secret names and any account identifier, and `createRequest` for endpoint and -sender requirements. Some adapters accept a complete operation URL; others append an operation +sender requirements. Some adapters accept a complete operation URL. Others append an operation path to a **base URL**. Use the approved HTTPS form expected by that adapter. For base URLs, reject an already-appended operation suffix, query or fragment, and reject or normalize a trailing slash before freezing settings so the operation path appears exactly once. -Set an account/destination-approved sender where required; an adapter's default sender is not +Set an account and destination-approved sender where required. An adapter's default sender is not proof of approval. These requirements apply equally to primary and secondary roles. Store no credential values in the configuration. Attach each referenced user-assigned managed identity to the Function App. Grant the vault-reading identity scoped secret-read access, such as Key Vault Secrets User under RBAC or the approved access-policy equivalent. If using the system-assigned identity, grant that identity instead. Client-ID settings do not attach -identities or grant access. Complete provider account, recipient, sender and consent prerequisites; +identities or grant access. Complete provider account, recipient, sender, and consent prerequisites. see [provider onboarding](ONBOARDING.md#complete-provider-authentication). ## 3. Credential isolation Open [credentials.js](../javascript/src/functions/credentials.js). When caching is enabled, its -singleton retains the first selected configuration; passing another account does not switch that cache. +singleton retains the first selected configuration. Passing another account does not switch that cache. Create a long-lived service for each account instead, with an immutable configuration: ```javascript @@ -182,7 +182,7 @@ PRIMARY_PRE_DISPATCH_FAILURE or VERIFIED_NONACCEPTANCE ``` All transitions require the expected state/version and valid ownership. A duplicate invocation -must not send; handle its response using the agreed operation contract. Never reset an intent +must not send. Handle its response using the agreed operation contract. Never reset an intent or uncertain dispatched attempt after a timeout, lease expiry or restart. An abandoned intent may mean a message was sent even if no result was saved. Fail closed when the store is unavailable. Do not let stale owners send after a claim is revoked or let a new owner reclaim a reserved attempt. @@ -191,8 +191,8 @@ Record operation state, safe provider context ID and per-attempt classifications request logs. Do not store/log bodies, OTPs or credentials as attempt evidence. Retention and key reuse rules must cover the caller's replay window. A store guard cannot atomically commit an external provider send: it is **not provider-side idempotency or true exactly-once delivery**. -Entra ID retries or native fallback outside this endpoint can still produce duplicates; confirm that -behavior with the owner rather than promising the local guard prevents it. +Microsoft Entra ID retries or native fallback outside this endpoint can still produce duplicates. +Confirm that behavior with the owner rather than promising the local guard prevents it. @@ -241,9 +241,9 @@ return CUSTOMER_FINAL_RESPONSE_WITHOUT_ANOTHER_ATTEMPT(secondary); Every `CUSTOMER_*` helper, result category, classifier, store and deadline here is new customer code. `CUSTOMER_ATTEMPT_ONCE` must reserve/persist transport intent and recheck ownership and remaining budget immediately before sending. Only the primary's approved pre-dispatch or proven -nonacceptance outcome can reserve a secondary; secondary failure is final. +nonacceptance outcome can reserve a secondary. Secondary failure is final. Persist each outcome, including policy/budget denial, before the final response. If saving a -result fails after intent, leave that attempt non-reclaimable; do not send through the secondary. +result fails after intent, leave that attempt non-reclaimable. Do not send through the secondary. **Do not use `result.httpStatus >= 400` or `catch -> sendSecondary`.** [providerResult.js](../javascript/src/functions/providerResult.js) exposes `Continue`, `Fail` @@ -255,13 +255,13 @@ reinterpret generic `Fail` or a broad status range as proof. [providerTransport.js](../javascript/src/functions/providerTransport.js) collapses fetch and response-body failures into `provider_timeout`/`provider_network_error`. Those errors do **not** expose whether the request was transmitted. Treat them as uncertain after intent, including -connection errors; do not guess they happened before send. Treat a primary credential failure +connection errors. Do not guess they happened before send. Treat a primary credential failure as pre-dispatch only when the flow and operation state prove transport was never entered. The failure must also be isolated to the primary, and account policy must still allow the secondary. Keep endpoint allowlists, HTTPS checks, manual redirects and per-provider response mapping. Never forward inbound authorization. Preserve the endpoint success nonce/correlation response -only for evaluation or an accepted delivery result; terminal/uncertain outcomes get no success +only for evaluation or an accepted delivery result. Terminal and uncertain outcomes get no success nonce. Continue using [safe logging](../javascript/src/functions/logging.js), with an explicit per-attempt provider ID/classification and one final request outcome. @@ -298,8 +298,8 @@ node --test .\javascript\test\provider-flow.test.js ` After customization, adapt the original singleton/lifecycle fixtures and extend the [adapter](../javascript/test/provider-flow.test.js), [credential](../javascript/test/credential-cache.test.js) and -[handler](../javascript/test/sendotp.test.js) tests. Use fake credentials, transport, clocks and -store faults; block external network. Verify: +[handler](../javascript/test/sendotp.test.js) tests. Use fake credentials, transport, clocks, and +store faults. Block external network. Verify: - Primary acceptance means zero secondary calls. Approved credential-before-transport failure or explicitly modeled nonacceptance allows at most one secondary, only with guard and budget. @@ -307,12 +307,12 @@ store faults; block external network. Verify: dispatches mean zero secondary calls. Secondary failure never restarts the sequence. - Concurrent/retried logical operations and process restarts cannot reclaim intent/reservations. Store failure, deadline exhaustion and cancellation fail closed. -- Evaluation returns before operation claims/provider calls; incomplete/decryption failures remain - errors. Credentials, endpoints and wire responses stay isolated between accounts. +- Evaluation returns before claiming an operation or calling a provider. Incomplete requests and + decryption failures remain errors. Credentials, endpoints, and wire responses stay isolated between accounts. -Fake classifiers/stores cannot prove provider nonacceptance or distributed durability. -Routing/isolation tests alone are **not fallback tests**. No production classifier is supplied; -keep response-based fallback disabled until reviewed. +Fake classifiers and stores cannot prove provider nonacceptance or distributed durability. +Routing and isolation tests alone are **not fallback tests**. No production classifier is supplied. +Keep response-based fallback disabled until reviewed. Deploy one app/package at `/api/SendOtp` for code-owned policy, orchestrator, classifier, or store changes. Validate startup configuration on controlled restart. Verify cache refresh for rotation and update @@ -321,10 +321,10 @@ pinned references explicitly. Unchanged approved tests or Markdown edits need no Preserve inbound authentication and origin/network restrictions, including any Front Door forwarding. Regional failover is separate from provider fallback. Before live use, review account permissions/consent, sender/recipient entitlement, durable-store guarantees and caller -retry/native-fallback behavior. Rotate any exposed credentials before testing. Any Entra ID sign-in or +retry/native-fallback behavior. Rotate any exposed credentials before testing. Any Microsoft Entra ID sign-in or provider test needs separate authorization, an approved recipient, explicit attempt limits and stop conditions for uncertainty. HTTP 200/`PENDING` is not handset receipt. Nothing here authorizes -a send or proves Entra ID integration, capacity, regional resilience or live delivery. +a send or proves Microsoft Entra ID integration, capacity, regional resilience or live delivery. ## .NET and Python integration pointers @@ -342,7 +342,7 @@ the JavaScript behavior transfers unchanged. [read_config(env)](../python/src/config.py) and [PhoneProviderBase](../python/src/provider.py). Pass explicit account contexts instead of rereading process settings. Use a separate [CredentialTokenService](../python/src/credentials.py) and -[SecretResolver(env)](../python/src/secrets.py) per account; update warmup/shutdown. +[SecretResolver(env)](../python/src/secrets.py) per account. Update warmup and shutdown. Both runtimes still need customer-owned durable operation state, deadline enforcement and reviewed fallback classification. These pointers are not evidence that fallback is implemented or validated in either runtime. diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 945e8ae..646e40d 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -2,8 +2,7 @@ [Documentation](README.md) | [Deployment guide](GETTING-STARTED.md) | [Troubleshooting](../setup/docs/Troubleshooting.md) -Use this page after deployment to connect the provider, validate the endpoint, and hand it to -operations. +After deployment, connect the provider, validate the endpoint, and assign operational owners. | Task | Go to | | --- | --- | @@ -47,7 +46,7 @@ renewal. | --- | --- | --- | | Customer tenant ID | Microsoft Entra admin center > Overview | `-TenantId` | | Azure subscription ID | Azure portal > Subscriptions | `-SubscriptionId` | -| Endpoint application client ID | Entra > App registrations > application > Overview | `-ApplicationId` | +| Endpoint application client ID | Microsoft Entra admin center > App registrations > application > Overview | `-ApplicationId` | | Azure region | Available Linux FC1/EP1 region that meets quota and organization requirements | `-Location` | | Provider | Approved Security Store integration | `-Provider telesign` or `soprano` | | Provider route | Provider-confirmed Global or EU route | `-EndpointRegion global` or `eu` | @@ -259,5 +258,5 @@ the deployed validation checks. The handler has no fallback token validator. Never use `common` or `organizations` as the issuer, derive trust from request fields, or exempt `SendOtp`. -Keep the app registration's `tokenEncryptionKeyId` null. Microsoft Entra bearer-token encryption +Keep the app registration's `tokenEncryptionKeyId` null. Microsoft Entra ID bearer-token encryption is separate from the JWE delivery payload and is not supported by this sample. diff --git a/docs/SIGN-IN-MONITORING.md b/docs/SIGN-IN-MONITORING.md index c5eee73..7ec7682 100644 --- a/docs/SIGN-IN-MONITORING.md +++ b/docs/SIGN-IN-MONITORING.md @@ -1,9 +1,9 @@ -# Monitor Microsoft Entra sign-ins +# Monitor Microsoft Entra ID sign-ins [Documentation](README.md) | [Function monitoring](MONITORING.md) -Use this page to investigate sign-ins and create identity alerts. Function execution, provider -acceptance, and delivery are monitored separately. +Sign-in records explain identity activity. Monitor Function execution, provider acceptance, and +delivery through their own data sources. | Task | Start here | | --- | --- | @@ -229,7 +229,7 @@ Missing evidence does not mean fewer failures. ## Privacy, licensing, and costs -- Microsoft Entra roles and Azure RBAC are separate. +- Microsoft Entra ID roles and Azure RBAC are separate. - External tenants have different subscription and export arrangements. - Risk and premium report features can require additional licensing and may not be available in external tenants. diff --git a/dotnet/README.md b/dotnet/README.md index 0b1ddd4..2340f6e 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -18,7 +18,7 @@ Target: .NET 8 isolated worker, Azure Functions v4. from [Providers/](Src/Providers/), configure it, and complete [provider authentication](../docs/ONBOARDING.md#complete-provider-authentication). 2. Build [dotnet.csproj](dotnet.csproj), run [xUnit tests](tests/Epp.Otp.Tests.csproj), and start - Core Tools here, **on loopback only, without tunnels**; it has no Easy Auth. + Core Tools here, **on loopback only, without tunnels**. It has no Easy Auth. 3. For manual deployment, publish to a compatible .NET isolated Function App. Inspect the output for private settings and complete the [Easy Auth trust checklist](../docs/ONBOARDING.md#manual-deployment-trust-checklist), then run @@ -49,10 +49,10 @@ For live delivery, use the [shared settings table](../TECHNICAL.md#configure-env for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options. Keep values as strings, including `EPP_PROVIDER_TIMEOUT_MS`, for example `"1500"`. Replace placeholders and store credentials in the adapter's Key Vault secrets. Each selected -channel requires a valid endpoint; an invalid or missing endpoint returns an error without using +channel requires a valid endpoint. An invalid or missing endpoint returns an error without using the other channel. -Core Tools loads `Values` into environment variables; direct worker execution and tests do not. +Core Tools loads `Values` into environment variables. Direct worker execution and tests do not. Restart after edits. Use a local test PEM because Core Tools cannot resolve Key Vault references. The [project](dotnet.csproj) excludes local settings from publish output. @@ -62,9 +62,8 @@ Provider credentials use managed identity, not a developer CLI login. Keep local ## Request behavior `POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api). -Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other -parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and -does not echo it. +Easy Auth authenticates the caller. The handler then validates and decrypts the payload. It never +forwards or returns the incoming `Authorization` header. **.NET binding runs first:** `[FromBody]` creates `EntraSendOtpPayload`. Malformed JSON, invalid enum tokens, or other deserialization errors can fail before `SendOtp`, its `ILogger` events, @@ -86,16 +85,16 @@ Keep caller authentication and public responses in the shared handler. For deployed diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and [runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery). -The host's OpenTelemetry setting does not by itself initialize a direct worker exporter; +The host's OpenTelemetry setting does not by itself initialize a direct worker exporter. verify actual `ILogger` collection before depending on event-based alerts. `SendOtp` validates and decrypts, selects `EPP_PROVIDER_NAME`, resolves credentials, and calls the provider. `PhoneProviderBase.SendJsonAsync` owns bounded HTTP transport and typed response -mapping; each provider maps its statuses to `ProviderResult`. Request-aware routing or multiple +mapping. Each provider maps its statuses to `ProviderResult`. Request-aware routing or multiple accounts require a separate trust, configuration, credential, and test design. `CredentialTokenService` warms and caches the selected provider credential. The shared cache -switches default to `true`; disabling the selected switch skips warmup and fetches on every live +switches default to `true`. Disabling the selected switch skips warmup and fetches on every live request. Enabled credentials remain in `MemoryCache` until absolute expiry. There is no polling timer, and concurrent misses can fetch more than once. The fetch has a 2.5-second cancellation budget. See [credential caching](../TECHNICAL.md#provider-settings-and-credential-caching). @@ -111,14 +110,14 @@ budget. See [credential caching](../TECHNICAL.md#provider-settings-and-credentia | [Src/OtpLog.cs](Src/OtpLog.cs) | Source-generated, strongly typed [structured logging events](../docs/LOGGING.md#event-sequence) | | [Src/PhoneProviderBase.cs](Src/PhoneProviderBase.cs) | Provider extension contract and shared typed JSON/HTTP transport | | [Src/Providers/](Src/Providers/) | Provider identity, credential delegation and API-specific request/response protocols | -| [Src/SecretResolver.cs](Src/SecretResolver.cs) | Key Vault transport; `ISecretResolver.ResolveAsync` accepts cancellation | +| [Src/SecretResolver.cs](Src/SecretResolver.cs) | Key Vault transport. `ISecretResolver.ResolveAsync` accepts cancellation. | | [Src/Models.cs](Src/Models.cs) | Outcomes and shared records | Derive from `PhoneProviderBase`, register the provider in [Program.cs](Program.cs), and give it the configured name used by the simple `SelectProvider` policy in `SendOtp`. Providers own authentication declaration, credential resolution, request construction and private typed response DTOs. Their public `SendOtpAsync` method delegates to the base `SendJsonAsync` transport and maps the typed response into -`ProviderResult`; provider mapping code never handles `HttpResponseMessage` or raw JSON DOM types. +`ProviderResult`. Provider mapping code never handles `HttpResponseMessage` or raw JSON DOM types. Each provider keeps its own response rules. Common async HTTP transport lives in `PhoneProviderBase`. See [production readiness](../TECHNICAL.md#production-readiness) when planning a live service. diff --git a/javascript/README.md b/javascript/README.md index acceb5f..36c4741 100644 --- a/javascript/README.md +++ b/javascript/README.md @@ -31,7 +31,7 @@ provider per deployment. Run Core Tools from `javascript/`. Create an untracked `local.settings.json` **beside [host.json](host.json), not inside `src/`**. Start from the -[shared sample](../docs/local.settings.sample.json); for local evaluation, start Azurite and replace +[shared sample](../docs/local.settings.sample.json). For local evaluation, start Azurite and replace the test-key placeholder in this minimal setup: ```json @@ -48,10 +48,10 @@ the test-key placeholder in this minimal setup: For live delivery, use the [shared settings table](../TECHNICAL.md#configure-environment-variables) for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options. Keep `Values` as strings, including `EPP_PROVIDER_TIMEOUT_MS`, for example `"1500"`. -Replace placeholders; Store API keys in Key Vault. Each selected channel requires a valid endpoint; an invalid or -missing endpoint returns an error without using the other channel. +Replace the placeholders. Store API keys in Key Vault. Each selected channel requires a valid +endpoint. An invalid or missing endpoint returns an error without using the other channel. -Core Tools copies `Values` into the process environment; direct Node processes and offline tests +Core Tools copies `Values` into the process environment. Direct Node processes and offline tests do not load the file. Restart after edits. Use a local test PEM because Core Tools cannot resolve Key Vault references locally. Keep Azurite settings local. @@ -61,9 +61,8 @@ Provider secrets use managed identity, not a developer CLI login. Keep local eva ## Request behavior `POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api). -Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other -parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and -does not echo it. +Easy Auth authenticates the caller. The handler then validates and decrypts the payload. It never +forwards or returns the incoming `Authorization` header. Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate authentication and decryption without a provider send. Live success confirms acceptance, @@ -84,15 +83,15 @@ The [cache switches and defaults](../TECHNICAL.md#provider-settings-and-credenti Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. -With caching enabled, API keys use Key Vault and `lru-cache`; OAuth uses the managed-identity/Entra -SDKs. A 30-second refresh loop and one in-flight acquisition support warm reads. See the +With caching enabled, API keys use Key Vault and `lru-cache`. OAuth uses managed identity and the +Azure Identity SDK. A 30-second refresh loop and one in-flight acquisition support warm reads. See the [credential caching guide](../TECHNICAL.md#provider-settings-and-credential-caching). | Source | Purpose | |---|---| | [src/functions/SendOtp.js](src/functions/SendOtp.js) | Explicit HTTP orchestration and startup/termination hooks | | [src/functions/config.js](src/functions/config.js) | Shared deployment settings | -| [src/functions/entraPayload.js](src/functions/entraPayload.js) | Validated Entra envelope and exact contract reasons | +| [src/functions/entraPayload.js](src/functions/entraPayload.js) | Validated Microsoft Entra ID envelope and exact contract reasons | | [src/functions/delivery.js](src/functions/delivery.js) | Validated decrypted context and immutable provider-neutral delivery | | [src/functions/jwe.js](src/functions/jwe.js) | Flat JWE validation/decryption with pinned algorithms and PEM key cache | | [src/functions/providerTransport.js](src/functions/providerTransport.js) | HTTPS validation, bounded fetch, body read, JSON parse and manual redirects | diff --git a/python/README.md b/python/README.md index 650674a..0809aa4 100644 --- a/python/README.md +++ b/python/README.md @@ -18,7 +18,7 @@ Target: Python 3.11, Azure Functions v4, Python v2 programming model. [providers/](src/providers/), configure it, and complete [provider authentication](../docs/ONBOARDING.md#complete-provider-authentication). 2. Use a virtual environment, install [requirements.txt](requirements.txt) and pytest, and run - [tests/](tests/). Start Core Tools here, **on loopback only, without tunnels**; it has no Easy Auth. + [tests/](tests/). Start Core Tools here, **on loopback only, without tunnels**. It has no Easy Auth. 3. For manual deployment, publish to a compatible Linux Python Function with dependencies or remote build. Inspect the package for private settings, complete the [Easy Auth trust checklist](../docs/ONBOARDING.md#manual-deployment-trust-checklist), and run @@ -47,11 +47,11 @@ For local evaluation, start Azurite and replace the test-key placeholder in this For live delivery, use the [shared settings table](../TECHNICAL.md#configure-environment-variables) for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options. -Keep values as strings, including `EPP_PROVIDER_TIMEOUT_MS: "1500"`. Replace placeholders; +Keep values as strings, including `EPP_PROVIDER_TIMEOUT_MS: "1500"`. Replace the placeholders. Store API keys in provider-named Key Vault secrets. Each selected channel requires a valid -endpoint; an invalid or missing endpoint returns an error without using the other channel. +endpoint. An invalid or missing endpoint returns an error without using the other channel. -Core Tools loads `Values` into `os.environ`; direct Python and pytest do not. Restart after edits. +Core Tools loads `Values` into `os.environ`. Direct Python and pytest do not. Restart after edits. Use a local test PEM because Core Tools cannot resolve Key Vault references locally. Keep Azurite settings local. @@ -61,9 +61,8 @@ Provider secrets use managed identity, not a developer CLI login. Keep local eva ## Request behavior `POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api). -Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other -parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and -does not echo it. +Easy Auth authenticates the caller. The handler then validates and decrypts the payload. It never +forwards or returns the incoming `Authorization` header. Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate authentication and decryption without a provider send. Live success confirms acceptance, @@ -85,21 +84,21 @@ The [cache switches and defaults](../TECHNICAL.md#provider-settings-and-credenti Disabling the selected cache removes startup preparation, polling, and cross-request reuse, not live credential acquisition. -With caching enabled, API keys use Key Vault and `cachetools.TTLCache`; OAuth uses the -managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the +With caching enabled, API keys use Key Vault and `cachetools.TTLCache`. OAuth uses managed identity +and the Azure Identity SDK. A daemon polls every 30 seconds. See the [credential caching guide](../TECHNICAL.md#provider-settings-and-credential-caching). | Source | Purpose | |---|---| | [function_app.py](function_app.py) | Typed request orchestration and direct provider selection | | [src/config.py](src/config.py) | Shared deployment settings | -| [src/models.py](src/models.py) | Typed Entra payload, delivery context, provider request and result dataclasses | +| [src/models.py](src/models.py) | Typed Microsoft Entra ID payload, delivery context, provider request and result dataclasses | | [src/jwe.py](src/jwe.py) | Pinned JWE decryption and typed delivery-context conversion | | [src/provider.py](src/provider.py) | Shared HTTPS transport, timeout handling and endpoint status mapping | | [src/credentials.py](src/credentials.py) | `CredentialTokenService`, `ApiKeyCache` and `AccessTokenCache` | | [src/otp_log.py](src/otp_log.py) | Fixed standard-logging event definitions and immutable request context | | [src/providers/](src/providers/) | Provider-owned credentials, requests and response mapping | -| [src/secrets.py](src/secrets.py) | Key Vault transport; bundle caching belongs to `ApiKeyCache` | +| [src/secrets.py](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 From 59ebcebdb4bb2ad04cb905ee622f05d1fce87189 Mon Sep 17 00:00:00 2001 From: Hou Chi Chan Date: Thu, 15 Oct 2026 16:33:52 -0700 Subject: [PATCH 5/5] docs: focus Front Door guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe53050-0d2b-4d05-bcc1-20f5b611b665 --- docs/FRONTDOOR.md | 130 +++++++++++++--------------------- setup/docs/Troubleshooting.md | 4 +- 2 files changed, 51 insertions(+), 83 deletions(-) diff --git a/docs/FRONTDOOR.md b/docs/FRONTDOOR.md index e959b45..41918a6 100644 --- a/docs/FRONTDOOR.md +++ b/docs/FRONTDOOR.md @@ -15,20 +15,17 @@ Azure Front Door provides one public EPP URL across regional Function Apps. Star | 3 | [Add a non-delivering readiness endpoint](#3-provide-a-non-delivering-readiness-endpoint) | | 4 | [Configure Front Door](#4-configure-front-door-manually) | | 5 | [Validate the complete route](#5-validate-before-manually-activating-policy) | -| 6 | [Run a failover drill](#6-regional-failover-fire-drill) | +| 6 | [Validate recovery](#6-validate-recovery) | -[Historical test results](#observed-failover-results-and-limitations) are context, not an SLA. +## When to use this design -## Scope and observed behavior +Use this advanced manual design when one public endpoint must route across regional Function Apps. +The standard setup creates one regional endpoint. Released packages do not include the readiness +route required by Front Door health probes, so implement and validate that route before enabling +multi-region routing. -The recorded tests used JavaScript, Front Door Standard, two or three regions, one shared -encryption key, and a non-delivering readiness handler. Released packages remain single-region, -so add and validate the readiness route before enabling probes. - -### Regional resiliency responsibility - -Customers own capacity, credentials, keys, monitoring, drills, and rollback. Set recovery, -latency, capacity, failure, and evidence targets before testing. +Plan capacity, credentials, keys, monitoring, recovery tests, and rollback for the complete design. +Set measurable recovery, latency, capacity, and failure targets before activation. ## Architecture @@ -49,12 +46,12 @@ or version IDs. The caller uses `https:///api/SendOtp`. Front Door selects an origin by health, priority, latency, and weight. Equal weights do not guarantee a 50/50 split. -### Two-region baseline to provision and validate +### Regional origins -Start with two equal-priority regional stacks. Each region has its own Function App, capacity, -storage, Key Vault, identities, telemetry, matching key, and provider credentials. Verify each can -operate without the other. Front Door, Microsoft Entra ID authentication, and the provider remain -shared dependencies. +Give every region its own Function App, capacity, storage, Key Vault, identities, telemetry, and +provider credentials. Use equal priority when the regions should all serve traffic, and verify +that every supported survivor combination can handle the required workload. Front Door, +Microsoft Entra ID authentication, and the provider remain shared dependencies. ## 1. Plan the regional deployment @@ -70,7 +67,7 @@ Build and test the regional design outside live traffic. Review the - Use one runtime and the same package in every region. - Record the endpoint application's client ID, validated token audience and issuer, authorized caller application ID, certificate/public-key identity, selected provider route, and regional owners. -- Plan rollout, validation, renewal, monitoring, drills, and rollback before activation. +- Plan rollout, validation, renewal, monitoring, recovery tests, and rollback before activation. Follow this regional procedure rather than rerunning `Setup-Epp.ps1` against the same application. The setup script creates one region. The steps below coordinate Front Door, shared @@ -97,7 +94,7 @@ whether the design meets the agreed capacity target. Validate the target deploym measured load result. Encrypted evaluation requests bypass request-path provider credential resolution and outbound -provider calls. A double-load evaluation drill proves only the exercised ingress, authentication, +provider calls. An evaluation load test covers only the exercised ingress, authentication, decryption, and routing path, **not complete live-send capacity**. Background credential refresh can still run independently. Its presence does not prove live-path credential capacity. @@ -106,7 +103,7 @@ provider-approved sandbox or stub**. Exercise the live request path, credential provider delay, and quota or throttling behavior. If the test system cannot match part of production, record the difference and add provider capacity evidence. Include cold starts, cache expiry, and key refresh so warm caches do not hide a regional dependency. Keep this test separate -from the evaluation-only fire drill, and use live sends only through a separately approved test. +from the evaluation-only recovery test, and use live sends only through a separately approved test. ## 2. Prepare regional origins @@ -177,15 +174,14 @@ Readiness confirms that the handler and decryption key are usable. Validate prov and handset delivery separately. Point probes to the implemented readiness handler, return a live result rather than a cached static `200`, and keep Easy Auth enabled for SendOtp. Front Door treats only a `200` probe response as healthy. -If every origin is unhealthy, Front Door can still route requests across them. Keep authentication -and application failure handling effective independently of probe status. +Review [failover behavior](#failover-behavior) before setting recovery expectations. ## 4. Configure Front Door manually In the Azure portal, create a Front Door Standard/Premium profile, an endpoint, and an origin group. -These are the **tested starting settings**. Validate them against the required performance target: +Use these starting settings, then validate them against the required performance target: -| Setting | Tested value | +| Setting | Starting value | |---|---| | Tier and domain | Standard, using its generated `azurefd.net` hostname | | Origin hostnames | Each Function App's actual default hostname | @@ -224,8 +220,8 @@ Before allowing Front Door to reach the Functions, configure their access restri 4. Open only the access needed by this public-origin design after the configuration is verified. A publicly resolvable hostname does not mean direct requests should be allowed. -Premium with Private Link is an alternative origin-security design, but was not tested here. -WAF policies and custom domains also need separate configuration and validation. +Premium with Private Link is another origin-security design. Configure and validate Private Link, +WAF policies, and custom domains separately when the deployment uses them. ## 5. Validate before manually activating policy @@ -240,7 +236,7 @@ nonces locally without writing them or request bodies to shared logs. that the rejection came from access control, not a general outage. - Valid encrypted evaluations must return the matching nonce. Malformed/tampered requests must not be accepted. Use safe correlation IDs and regional logs to confirm which origins handled requests. -- Run the [regional failover fire-drill](#6-regional-failover-fire-drill) in both directions, with +- Run the [recovery test](#6-validate-recovery) in both directions, with agreed acceptance criteria and evidence for the [capacity gate](#capacity-gate-each-region-must-support-the-combined-peak-load). - Separately test approved live SMS/voice delivery and caller behavior with the provider and EPP onboarding owner. Evaluation does not prove either. @@ -252,10 +248,9 @@ preserve unrelated properties, and read the policy back to confirm the change. T ### Test scenarios to plan and record -**Proposed tests, not completed results.** Historical [app-stop trials](#observed-failover-results-and-limitations) -do not prove double-load capacity, dependency isolation, or a regional outage. -Approve synthetic data, bounded load, and the [drill's abort/restoration safeguards](#6-regional-failover-fire-drill) -in isolated nonproduction. Agree load/headroom, recovery, errors/latency, soak, and restoration deadlines first. +Approve synthetic data, bounded load, and the +[recovery test's abort and restoration safeguards](#6-validate-recovery) in isolated +nonproduction. Agree load, headroom, recovery, error, latency, soak, and restoration targets first. Use an authorized harness, **not a real Microsoft Entra ID sign-in flow**. Evaluation suppresses sends only inside this endpoint. Caller fallback or retries can still deliver. Confirm caller behavior @@ -265,32 +260,31 @@ with Microsoft and obtain separate approval for sign-in or delivery tests. |---|---|---|---| | 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. | -| Failover from A to B | Follow the fire-drill: isolate only approved test origin A while leaving it **enabled in Front Door**. Keep B healthy and evaluation traffic bounded. | Health-based rerouting 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. | +| Failover 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. | | Failover from B to A | After A is fully restored and stable, repeat with B isolated but **enabled in Front Door**. | A meets the same agreed criteria. Both directions must pass. | The same evidence, identifying A as the survivor and confirming B's subsequent restoration. | | Sustained survivor capacity | In each direction, maintain the [combined peak offered load plus planned headroom](#capacity-gate-each-region-must-support-the-combined-peak-load) for the agreed soak. | **Each region handles at least double its normal share in the balanced two-region case, plus headroom**, within agreed error/latency and dependency limits. Evaluation proves only the exercised path. | Offered/completed rates, concurrency, latency percentiles, errors/timeouts, saturation/throttling, quotas and available capacity per region. | -| Restart, cold start, and credential refresh without the failed region | In a separately approved dependency-isolation test environment, make the failed region's storage/vault paths unavailable and start or restart the survivor stack. Exercise key resolution and credential refresh using the non-delivering provider setup below. Do not add a second fault to the basic fire-drill. | The survivor starts and serves using its own dependencies. Warm caches do not hide cross-region access. Missing failed-region telemetry is not treated as success. | Dependency configuration/access evidence, startup/key/credential-refresh results, client outcomes, and timestamps captured outside the failed region. | +| Restart, cold start, and credential refresh without the failed region | In a separately approved dependency-isolation test environment, make the failed region's storage/vault paths unavailable and start or restart the survivor stack. Exercise key resolution and credential refresh using the non-delivering provider setup below. Do not add a second fault to the basic recovery test. | The survivor starts and serves using its own dependencies. Warm caches do not hide cross-region access. Missing failed-region telemetry is not treated as success. | Dependency configuration/access evidence, startup/key/credential-refresh results, client outcomes, and timestamps captured outside the failed region. | | Complete live-path capacity and provider credential failures | Separately use a provider-approved, production-representative **non-delivering sandbox/stub** exercising credential acquisition/refresh and provider latency/quota behavior. Test valid and invalid/expired test credentials without production accounts or recipients. | Both survivor regions meet the capacity criteria on this path. Credential failures are surfaced, not reported as successful sends. Evaluation-only throughput cannot pass this test. | Credential/provider timing, failures/throttling, load results, and provider capacity/quota evidence for behavior the sandbox/stub cannot reproduce. | | Restoration and conservative failback | Restore the original approved state even after a failed test. Verify the restored origin's own readiness and successful evaluations, then observe traffic returning for the agreed window. | Both origins are healthy and serving within agreed limits, with original security settings intact, before any next fault. | Restoration confirmation, origin-specific readiness/request evidence and failback latency/errors. | -| Interrupted-controller recovery | Verify the independent restoration timer or backup operator before fault injection. In an approved drill, interrupt the controller while the independent safeguard remains available. | The fault is removed by the agreed deadline without relying on the interrupted controller. Unsafe load stops and unresolved recovery is escalated. | Interruption/restoration timestamps, safeguard or backup-operator actions, and both origins' final state. | For every row, record **pass, fail, or not tested**, the approved target, observed outcome, and evidence location. Assign an owner and retest date to gaps. A successful routing test is useful, but is not a substitute for the separate capacity and dependency tests. -## 6. Regional failover fire-drill + +## 6. Validate recovery -Run this drill in an approved nonproduction environment built for failover testing. It exercises -health-based rerouting without affecting production and covers the documented scenario rather than -every possible regional outage. Define numeric pass/abort thresholds and durations before starting -instead of treating the observed timings below as an SLA. +Run this test in an approved nonproduction environment built for recovery validation. It checks +health-based routing without affecting production. Define numeric pass, abort, and restoration +thresholds before starting. -1. **Approve the boundary.** Name the drill, change, telemetry, and restoration owners plus a +1. **Approve the boundary.** Name the test, change, telemetry, and restoration owners plus a backup. Record the exact test resources, window, isolation deadline, and restoration steps. Confirm no production policy or real users depend on them. Isolate only one origin at a time. 2. **Prepare safe traffic and recovery.** Use an authorized caller with bounded encrypted `mode: 2` evaluation traffic—never live sends or real recipient data. Set rate, concurrency, - timeout, duration, and cost limits. Keep the controller/evidence outside the target region and - verify an independent restoration mechanism or backup operator. + timeout, duration, and cost limits. Keep the test runner and evidence outside the target region and + verify a restoration timer or backup operator. 3. **Set the baseline and gates.** Save the approved routing, authentication, ingress, package, key, and dependency state. Confirm both origins are healthy and serving. Agree survivor load, soak time, recovery target, allowed failures/timeouts, latency/resource limits, failback window, @@ -303,7 +297,7 @@ instead of treating the observed timings below as an SLA. routing, and regional dependency signal. Missing telemetry is not success. Meet the full soak criteria. Evaluation-path results do not replace the separate live-path capacity evidence. 6. **Restore on completion or abort.** Stop unsafe load and restore the saved state at the deadline, - any abort threshold, or completion. Cleanup must run even if assertions or the controller fail. + any abort threshold, or completion. Cleanup must run even if assertions or the test runner fail. Escalate immediately if restoration cannot be confirmed. 7. **Verify failback, then reverse direction.** Check the restored origin's dependencies, key and credential resolution, readiness, and evaluation responses. Wait through the agreed stable @@ -324,48 +318,22 @@ that cannot be tested safely and assign an owner and follow-up date. Use separate approved checks for real Microsoft Entra ID caller timing, provider acceptance, and handset delivery. Define retry and availability targets with the provider and onboarding owner. -Front Door and this runbook do not guarantee zero downtime or make uncertain retries safe. -## Observed failover results and limitations + +## Failover behavior -We stopped one Function App in these JavaScript tests, **not an entire Azure region**. One test -client in Central US sent encrypted evaluation requests without delivering messages. The first two -origins were in Central US and West US 2. The third was in West US 3. -We stopped the Central US Function after confirming it was serving requests. All trials used -a sample size of 4 with 3 successes required to consider an origin healthy. +Front Door selects from eligible healthy origins using priority, latency, and weight. Health status +uses a sliding sample of probe results, and only HTTP `200` counts as healthy. Detection and routing +changes take time, so requests can fail during a transition. -| Origins | Probe interval | Failed requests in matched first 8 minutes | Sustained success after stop confirmation | -|---|---|---|---| -| 2 | 30 seconds | 32/478 (6.7%) | About 144 seconds | -| 2 | 10 seconds | 58/471 (12.3%) | About 154 seconds | -| 3 | 30 seconds | 33/462 (7.1%) | About 123 seconds | -| 3, repeat | 30 seconds | 32/460 (7.0%) | About 150 seconds | - -Transient request failures occurred during the controlled app-stop tests. Healthy origins continued -serving, and sustained successful responses resumed. No HTTP 5xx responses were observed. These -results show recovery in the tested scenarios, not uninterrupted service or resilience to a complete -regional outage. - -"Sustained success" means every sampled request after the last failure succeeded, with at least -30 seconds of successful traffic before we restarted the Function. We measured from when Azure -confirmed the stop, which may differ from when the Function actually stopped serving. -We aimed for one request per second, but slow responses and timeouts reduced the sample counts. -The test controller was interrupted during one three-origin trial, extending that outage while we -recovered it. To keep the comparison fair, the table uses only the first eight minutes of each outage. -We saw no failed samples before the outages or after restarting the Functions. - -A failed request is not guaranteed to be replayed on another origin. The actual Microsoft Entra ID caller's -handling of these errors was not tested. - -These trials did not show a consistent benefit from 10-second probes or a third region. -More regions can add capacity and help tolerate additional failures, but they do not remove routing -delays, shared provider dependencies, or application faults. Faster probes generate more traffic -and can make brief problems trigger routing changes sooner. Decide how much disruption your -application can accept, then repeat the tests from your callers' locations and at your expected load. - -Do not add blind retries to live sends: a timeout can occur after provider acceptance, and the sample -does not deduplicate deliveries. These results do not establish zero downtime, a production SLA, -or successful delivery to a phone. +If every origin is unhealthy, Front Door can route across all origins. Keep authentication, +application validation, and failure handling effective independently of probe status. Front Door +retries are best effort and do not guarantee that every failed request reaches another origin. + +Set recovery targets from your own approved tests at the required locations and workload. Do not +claim zero downtime, a fixed recovery time, or successful phone delivery from routing results. +Do not add blind retries to live sends because a timeout can occur after provider acceptance and +this application does not deduplicate deliveries. ## Operations and rollback diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md index c86b68a..1ddce6f 100644 --- a/setup/docs/Troubleshooting.md +++ b/setup/docs/Troubleshooting.md @@ -85,8 +85,8 @@ Follow the [Front Door guide](../../docs/FRONTDOOR.md), including readiness and network restriction. Never exempt SendOtp or disable Easy Auth. - An authenticated SendOtp 403 is a failure, not proof of failover. Check origin, ingress, and auth. - A shared-URL success does not prove every origin works; verify each with safe regional telemetry. -- Distinguish delayed aggregate metrics from event timestamps and client observations. See the - [recorded test results and limitations](../../docs/FRONTDOOR.md#observed-failover-results-and-limitations). +- Distinguish delayed aggregate metrics from event timestamps and client observations. Review + [failover behavior](../../docs/FRONTDOOR.md#failover-behavior) before setting recovery expectations. ## Deployment fails with SubscriptionIsOverQuotaForSku