diff --git a/TECHNICAL.md b/TECHNICAL.md index 1dcab2a..95ec59a 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -13,7 +13,11 @@ and managed-identity access to credentials. ## Single-region architecture -![Single-region External Phone Provider architecture](docs/images/single-region-architecture.png) +![Single-region component overview showing Microsoft Entra ID, Easy Auth, the Function and adapter, Key Vault, Application Insights, and the phone provider](docs/images/single-region-architecture.png) + +*Component overview, not a delivery timeline. The numbered delivery arrow does not mean +the Function waits for handset receipt; see the [request sequence](docs/BRING-YOUR-OWN-PROVIDER.md#end-to-end-endpoint-request) +for live SMS response ordering and the end user's verification steps.* Microsoft Entra ID sends an authenticated, encrypted request. Easy Auth checks the caller, then the Function decrypts the request using its Key Vault-backed key. For a live request, the @@ -25,6 +29,8 @@ with capacity and quota for your hosting plan. The provider-secret path applies integrations; OAuth integrations use managed-identity token exchange instead. Credential caching depends on the [selected cache settings](docs/CONTRACT.md#credential-caching-and-refresh), and [log formats vary by runtime](docs/MONITORING.md#runtime-specific-log-discovery). +Telemetry comes from the Function host/worker; the schematic telemetry arrow does not mean +Easy Auth rejections produce Function request logs. An authorized, valid encrypted **evaluation request (`mode: 2`)** returns the matching nonce without submitting a message to the provider. When caching is enabled, credential preparation diff --git a/docs/BRING-YOUR-OWN-PROVIDER.md b/docs/BRING-YOUR-OWN-PROVIDER.md index 7f6fb69..68bef93 100644 --- a/docs/BRING-YOUR-OWN-PROVIDER.md +++ b/docs/BRING-YOUR-OWN-PROVIDER.md @@ -9,6 +9,37 @@ Before deploying, ask your tenant administrator to confirm [eligibility, testing, and activation with Microsoft](ONBOARDING.md#before-purchasing-or-deploying). Deploying the endpoint alone does not enable tenant policy. +## End-to-end endpoint request + +This is a browser-based **SMS sign-in example after approved endpoint activation**. Your +application is the user's existing sign-in app, **not** the dedicated endpoint app registration +created during setup. Do not add a redirect URI to that endpoint registration to copy this flow. + +![End-user SMS sign-in sequence: a person opens an app, follows the redirect to Microsoft Entra ID, receives a provider-delivered SMS after an authorized SendOtp call, enters the code on the sign-in page, and accesses the app only after identity and application checks succeed. Provider acceptance and phone delivery are separate observations.](images/request-sequence.png) + +*Original UML sequence; [open the scalable diagram](images/request-sequence.svg). +Solid arrows are requests/delivery; dashed arrows are replies. The `par` frame separates +provider acceptance from recipient delivery; it does not promise their order or successful delivery. +This is a successful example, not a completed live test. Use the +[endpoint error reference](CONTRACT.md#epp-response-json) for rejected or failed endpoint requests.* + +The person reads the SMS on their phone and enters its code in the **Microsoft Entra ID sign-in +page**, as described in Microsoft's [text-message verification instructions](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-phone-options#text-message-verification). +Microsoft Entra ID checks the code and remaining sign-in requirements; the application validates +the sign-in result and applies its access rules. The +[browser redirect/token exchange](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#protocol-flow-sign-in) +belongs to that application, not this Function. + +The Function only delivers the supplied message. For live requests, its `200` response means provider acceptance, +**not phone receipt, a correct user-entered code, or application access**. Evaluation (`mode: 2`) +tests the endpoint without sending a message; it is not an end-user sign-in. +Voice and other sign-in flows can have different prompts; confirm their experience through the +[approved validation procedure](ONBOARDING.md#validate-the-deployed-endpoint), rather than assuming +they use this SMS screen or a generic voice-MFA gesture. + +The steps below explain the endpoint's part of the sequence. The +[architecture view](../TECHNICAL.md#single-region-architecture) shows its separate key and credential paths. + ## 1. Verify the caller @@ -188,7 +219,8 @@ or send-retry loop. Keep service secrets in Key Vault, not in settings or source 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 the offline tests](#run-the-offline-tests) + before building your package. 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) @@ -196,6 +228,26 @@ or send-retry loop. Keep service secrets in Key Vault, not in settings or source Retain the [Easy Auth trust checks](ONBOARDING.md#manual-deployment-trust-checklist), then complete the validation below. +### Run the offline tests + +Use a **full repository checkout**, not a downloaded Function release ZIP: the ZIPs omit tests, +and the suites use the shared `tests/fixtures/contract.json`. Run the unchanged suite first, +then rerun it after adding your integration. You do not need a tenant, app registration, +`local.settings.json`, Azurite, Core Tools, or provider account for these tests. + +Choose only your runtime's instructions: +[JavaScript](../javascript/README.md#run-offline-tests), +[C#](../dotnet/README.md#run-offline-tests), or +[Python](../python/README.md#run-offline-tests). Each gives the working folder, tools, and commands. +Use a terminal without production or deployed provider settings in its environment. +Dependency preparation needs the package registry; the tests use local test keys and +fake credentials/transport rather than Azure sign-in or live sends. + +Use the test runner's final summary and exit code: no collected tests, a missing module, or a +failed restore is not a passing run. These commands follow the repository's +[CI workflow](../.github/workflows/ci.yml). A pass checks the application, not deployed Easy Auth, +provider authorization, or delivery; continue with the separate acceptance checks below. + diff --git a/docs/FRONTDOOR.md b/docs/FRONTDOOR.md index f6020ab..d146062 100644 --- a/docs/FRONTDOOR.md +++ b/docs/FRONTDOOR.md @@ -44,7 +44,10 @@ approved by the deployment owners; the observed results below do not establish t ## Architecture: one URL, multiple origins -![Multi-region External Phone Provider architecture with Azure Front Door](images/multi-region-architecture.png) +![Multi-region component overview showing Microsoft Entra ID, Azure Front Door, two regional Function Apps with Key Vault and Application Insights, and a shared phone provider](images/multi-region-architecture.png) + +*Component overview, not a deployment template or delivery timeline. Match private-key material, +not vault version IDs or resource names. The green dashed arrows are responses, not health probes.* The diagram shows the design, not the exact test deployment. Its region names are examples. Custom domains and Premium/WAF are optional and weren't part of the Standard-tier test. @@ -54,6 +57,10 @@ consistent and use the same RSA key**, rather than copying every setting literal The SMS/voice arrows show live delivery. Evaluation returns the nonce without contacting the provider. The diagram's provider-secret arrows apply to API-key integrations; OAuth uses each region's outbound managed identity. Match the key material across regions, not the vault URLs or version IDs. +For response timing and separate recipient delivery, see the +[request sequence](BRING-YOUR-OWN-PROVIDER.md#end-to-end-endpoint-request). +Health probes require the [separate readiness handler](#3-provide-a-non-delivering-readiness-endpoint), +which is not shown in this overview or included in the release packages. | Component | Purpose | |---|---| diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index d63ebd6..00f51e8 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -98,10 +98,29 @@ Review the deployment plan, then retain `deployment-*.json` and the public certi The [setup output guide](../setup/docs/README.md#read-the-deployment-summary) explains the remaining fields. `policyChanged: false` is expected, not a failed deployment. -Inspect cloud settings in **Azure portal > Function App > Settings > Environment variables**. -They are setup-managed; do not recreate them from the local sample or bulk-copy another app's -settings. Do not export all settings into a ticket. Changes can restart the worker, and reruns -can restore setup-managed values. +To find your endpoint's cloud settings: + +1. In **Azure portal > Function Apps**, open the name in `resources.functionApp` in your saved + deployment summary. Check its subscription and resource group against `subscriptionId` and + `resources.resourceGroup`; do not select an app just because its display name looks similar. +2. Open **Settings > Environment variables > App settings**. Use the **Search** box to find + `EPP_PROVIDER_NAME`, `EPP_PROVIDER_SMS_ENDPOINT`, or `EPP_PROVIDER_VOICE_ENDPOINT`, then use + the table below to compare the approved routing configuration privately. +3. Find `EPP_DECRYPTION_KEY_PEM` by **Name**, but keep its value hidden. It is the private-key + reference, not a provider API key. Do not select **Show values** to export settings or include + them in a screenshot. Use the [key-reference checks](../setup/docs/Troubleshooting.md#certificate-selection-reuse-or-cleanup-fails) + if resolution fails. + +[![Official Azure portal example with Settings, Environment variables, and the App settings tab highlighted; all setting values remain hidden.](images/onboarding/function-app-settings.png)](images/onboarding/function-app-settings.png) + +*Navigation example from [Microsoft's Function settings instructions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings#get-started-in-the-azure-portal). +`function-app-1` is a demo name; use `resources.functionApp`. The generic host settings shown +are not an EPP checklist, and FC1/EP1 settings differ. +[Unmodified source](https://github.com/MicrosoftDocs/azure-docs/blob/1067858212cfae5f69de14fe53a3432eb461b052/articles/azure-functions/media/functions-how-to-use-azure-function-app-settings/azure-function-app-main.png), +Copyright (c) Microsoft Corporation, [CC BY 4.0](https://github.com/MicrosoftDocs/azure-docs/blob/1067858212cfae5f69de14fe53a3432eb461b052/LICENSE).* + +These settings are setup-managed; do not recreate them from the local sample or bulk-copy another +app's settings. Changes can restart the worker, and reruns can restore setup-managed values. | Value or setting | Source / verification | Customer action | |---|---|---| @@ -124,7 +143,9 @@ a missing, empty, or invalid selected endpoint fails closed. There are **three separate trust paths**: Easy Auth admits the Microsoft caller; the Key Vault certificate decrypts its JWE; provider credentials authorize outbound delivery. The HTTPS -certificate is separate again. Fix the failing path rather than replacing unrelated IDs or keys. +certificate is separate again. Use the [request sequence](BRING-YOUR-OWN-PROVIDER.md#end-to-end-endpoint-request) +to distinguish provider acceptance, phone receipt, and the user's code verification. Fix the failing path rather than +replacing unrelated IDs or keys. ## Complete provider authentication diff --git a/docs/images/onboarding/certificate-version.png b/docs/images/onboarding/certificate-version.png new file mode 100644 index 0000000..068a21f Binary files /dev/null and b/docs/images/onboarding/certificate-version.png differ diff --git a/docs/images/onboarding/function-app-settings.png b/docs/images/onboarding/function-app-settings.png new file mode 100644 index 0000000..6031c3b Binary files /dev/null and b/docs/images/onboarding/function-app-settings.png differ diff --git a/docs/images/request-sequence.png b/docs/images/request-sequence.png new file mode 100644 index 0000000..4914f99 Binary files /dev/null and b/docs/images/request-sequence.png differ diff --git a/docs/images/request-sequence.svg b/docs/images/request-sequence.svg new file mode 100644 index 0000000..9b1cdc4 --- /dev/null +++ b/docs/images/request-sequence.svg @@ -0,0 +1,88 @@ + + End-user sign-in: an SMS challenge + Successful browser sign-in example after approved endpoint activation. A person opens an existing application, follows its redirect to Microsoft Entra ID, and reaches an SMS challenge. Microsoft Entra ID calls the protected SendOtp endpoint with its own caller token and encrypted payload. The endpoint validates, decrypts, and sends through the configured provider. The acceptance response and delivery to the person's phone are separate observations with no guaranteed ordering. The person enters the received code into the Microsoft Entra ID sign-in page. Microsoft Entra ID checks it and remaining policy requirements; the application validates the sign-in result and its own access rules. The endpoint does not verify the user's code or sign the user in. + + + + + + + End-user sign-in: an SMS challenge + Successful example after approved activation, not a live-run trace. Not every sign-in requires SMS. + + + + + + + + End user + Browser + phone + Your application + Existing sign-in app + Microsoft Entra ID + Your endpoint + Easy Auth + Function + Phone provider + + Open app / select Sign in + + Redirect browser to sign-in + + + Start sign-in and complete earlier prompts + + + Show SMS verification page when policy requires it + + + POST /api/SendOtp + channel: 1, mode: 1 + + + Verify the Microsoft caller token, then validate and decrypt. + This is not the end user's token or a browser call. + Send SMS (own credentials) + + + + + par + + [acceptance response; not a delivery receipt] + Recognized acceptance + + HTTP 200 + original nonce + + + + [separate recipient delivery; no guaranteed order with the response] + + SMS reaches the person's phone, if delivery succeeds + + + + Read SMS; enter code on the Microsoft Entra ID sign-in page + + + + Check the code and remaining sign-in requirements + Return sign-in result + + App checks result + access + Open app only if authorized + + + + The Function delivers the supplied message; it does not verify the user's code or sign the user in. + Evaluation (mode 2) stops after endpoint validation/decryption: no provider send and no user SMS. + Earlier prompts, full token exchange, error paths and caller fallback are not expanded. Never blindly resend after a timeout. + diff --git a/dotnet/README.md b/dotnet/README.md index 5f647df..8e5419c 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -40,13 +40,27 @@ used JavaScript; validate an equivalent .NET readiness implementation and deploy before `SendOtp` runs. Those failures therefore do not produce application `ILogger` events or the handler's custom error response body; platform diagnostics and responses apply instead. 4. Build [dotnet.csproj](dotnet.csproj), run the offline xUnit suites in - [tests/Epp.Otp.Tests.csproj](tests/Epp.Otp.Tests.csproj), and start the local Functions host from this folder. + [tests/Epp.Otp.Tests.csproj](tests/Epp.Otp.Tests.csproj) using the + [offline test commands](#run-offline-tests), and start the local Functions host from this folder. Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding. 5. Publish this app folder to a compatible .NET isolated Function App. Inspect the package: both [.funcignore](.funcignore) and the project protect local settings; private keys must not be included. Offline tests cover application behavior, not platform authentication; run the separate [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify). +## Run offline tests + +For developer work, open `dotnet/` in a full repository checkout with the **.NET 8 SDK**. +Follow the [shared offline prerequisites](../docs/BRING-YOUR-OWN-PROVIDER.md#run-the-offline-tests); +you do not need to start the Functions host or configure a provider. + +```powershell +dotnet test .\tests\Epp.Otp.Tests.csproj +``` + +`dotnet test` restores and builds the test project, which compiles the handler and shared source +directly. Also run `dotnet build` in `dotnet/` before packaging to check the actual Function project. + ## Environment configuration Run Core Tools from `dotnet/`. Create an untracked `local.settings.json` beside diff --git a/javascript/README.md b/javascript/README.md index 460301f..accd126 100644 --- a/javascript/README.md +++ b/javascript/README.md @@ -36,13 +36,26 @@ sample's release package. No Front Door deployment script is supplied. trusted tenant issuer, endpoint-app `allowedAudiences` and a nonempty `allowedApplications` list for the authorized Microsoft caller. Do not exclude SendOtp. There is no backup application token validation; never expose the endpoint to the public internet with Easy Auth disabled or bypassed. -5. Install dependencies from [package.json](package.json), run its offline test script and start the +5. Install dependencies from [package.json](package.json), run the + [offline test commands](#run-offline-tests), and start the local Functions host from this folder. Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding. Publish this app folder only, preserving dependencies and observing [.funcignore](.funcignore); inspect the package before upload. Offline tests cover application behavior, not platform authentication; run the separate [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify). +## Run offline tests + +For developer work, open `javascript/` in a full repository checkout with **Node.js 20 or 22 +and npm**. Follow the [shared offline prerequisites](../docs/BRING-YOUR-OWN-PROVIDER.md#run-the-offline-tests); +you do not need to start the Functions host or configure a provider. + +```powershell +# First checkout or changed lockfile: +npm ci +npm test +``` + ## Environment configuration Run Core Tools from `javascript/`. Create an untracked `local.settings.json` **beside diff --git a/python/README.md b/python/README.md index 8a2e810..599dae2 100644 --- a/python/README.md +++ b/python/README.md @@ -35,13 +35,27 @@ used JavaScript; validate an equivalent Python readiness implementation and depl caller. Do not exclude SendOtp. There is no backup application token validation; never expose the endpoint to the public internet with Easy Auth disabled or bypassed. 4. Use a virtual environment, install [requirements.txt](requirements.txt) and pytest, then run the - offline [tests/](tests/) from this folder. Start the local Functions host from this app root. + offline [tests/](tests/) from this folder using the + [offline test commands](#run-offline-tests). Start the local Functions host from this app root. Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding. 5. Publish this folder to a compatible Linux Python Function App with dependencies or a supported remote build. Inspect the package and apply [.funcignore](.funcignore); keep local settings and keys private. Offline tests cover application behavior, not platform authentication; run the separate [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify). +## Run offline tests + +For developer work, open `python/` in a full repository checkout with **Python 3.11 in an +activated virtual environment**. Follow the +[shared offline prerequisites](../docs/BRING-YOUR-OWN-PROVIDER.md#run-the-offline-tests); +you do not need to start the Functions host or configure a provider. + +```powershell +# First checkout or changed requirements: +python -m pip install -r requirements.txt pytest +python -m pytest tests +``` + ## Environment configuration Run Core Tools from `python/`. Create an untracked `local.settings.json` beside diff --git a/setup/docs/README.md b/setup/docs/README.md index 2e74f09..d093e8c 100644 --- a/setup/docs/README.md +++ b/setup/docs/README.md @@ -302,12 +302,34 @@ Interactive selection never defaults to the newest certificate or to cleanup. En stops before deployment; invalid locations can be corrected or followed by another choice. Selecting an action does not apply it: the complete deployment approval lists the exact removals. -For reuse, open **Azure portal > Key Vault > Certificates > certificate > version** and copy -**Certificate Identifier**, of the form -`https://.vault.azure.net/certificates//`. A version is required. -Do not supply a vault-only URL, `/keys/` URL, public `.cer` file, secret value, or a versionless -reference. Setup reads the public certificate and resolves the corresponding **versioned backing -secret**; the certificate and secret version identifiers need not be identical. +**Reuse path only:** choose **Reuse** interactively or pass `-CertificateAction Reuse`. Unattended +reuse requires both `-EncryptionKeyId` and `-KeyVaultCertificateId`. + +1. Select the application's encryption credential key ID from setup's list or the app manifest's + `keyCredentials[].keyId`. This is the saved `encryptionKeyId`, not the application client ID. +2. In **Azure portal > Key vaults**, open the certificate's source vault. The saved `certificateId` + identifies its vault, certificate name, and version; this can differ from the provider-credential + vault in `resources.keyVault`. Open **Objects > Certificates**, select that certificate, then + select the exact version matching the app credential, not automatically **Current version**. + Without a saved summary, ask the certificate owner for the vault and version matching the + selected public thumbprint. +3. On **Certificate Version**, copy **Certificate Identifier** using the copy icon at the end of + that row. Use the complete HTTPS URL for `-KeyVaultCertificateId`; it must include the + `/certificates//` path. Do not copy **Key Identifier** or **Secret Identifier**, + and do not select **Download in PFX/PEM format**. + +[![Official Key Vault Certificate Version page showing Certificate Identifier above the separate Key Identifier and Secret Identifier fields, using Microsoft's AKV-Contoso and ExampleCertificate demo.](../../docs/images/onboarding/certificate-version.png)](../../docs/images/onboarding/certificate-version.png) + +*Field-location example from [Microsoft's certificate instructions](https://learn.microsoft.com/en-us/azure/key-vault/certificates/quick-create-portal#add-a-certificate-to-key-vault); +do not follow its certificate-creation or private-key-export steps for this sample. +The demo names, IDs, and expired dates are not reuse inputs; use your own valid, matching version. +[Unmodified source](https://github.com/MicrosoftDocs/azure-docs/blob/22e0ade52c247e5d306a78dc9e392d901eb9c198/articles/key-vault/media/certificates/quick-create-portal/current-version-hidden.png), +Copyright (c) Microsoft Corporation, [CC BY 4.0](https://github.com/MicrosoftDocs/azure-docs/blob/22e0ade52c247e5d306a78dc9e392d901eb9c198/LICENSE).* + +A version is required. Do not supply a vault-only URL, `/keys/` URL, public `.cer` file, secret value, +or a versionless reference. Setup reads the public certificate and resolves the corresponding +**versioned backing secret** for `EPP_DECRYPTION_KEY_PEM`; certificate and secret version identifiers +need not be identical. The Function's private-key reference is an output, not a value to download. The certificate must exactly match the selected registered certificate, not just its display name or RSA key. It must use exportable RSA of at least 2048 bits with an enabled,