Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ on:
pull_request:

jobs:
cyot-setup:
name: CYOT setup (PowerShell)
runs-on: windows-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Check standalone setup scripts (offline)
shell: pwsh
run: .\tests\setup\Test-CyotScripts.ps1

javascript:
name: JavaScript (Node.js)
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ node_modules/
# Local helper scripts
scripts/

# Generated CYOT setup files (the standalone setup scripts remain tracked)
/setup/cyot/*.backup-*.ps1
/setup/cyot/*.cer
/setup/cyot/*.crt
/setup/cyot/*.zip
/setup/cyot/cyot-policy-before-*.json
/setup/cyot/arm/*.local.json
/setup/cyot/arm/parameters.json

# Python
.venv/
venv/
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ by default. Deploy each language separately, not all three to the same Function
New here? Start with **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — setup, config, running, securing,
and deploying, step by step.

CYOT registration, provisioning and policy scripts are in **[setup/cyot/](setup/cyot/)**.
Step 2 is one entry point that runs both JSON templates in its companion `arm` folder.
Steps 1 and 3 remain independently runnable; no shared helper script is required. Read the
[compatibility limitations](setup/cyot/README.md#compatibility-with-this-sample) before provisioning:
the imported workflow is not yet a compatible end-to-end deployment path for these implementations.

## The design in one line

SAS → Easy Auth → anonymous HTTP handler (`POST /api/SendOtp`, validate envelope + decrypt JWE) →
Expand Down Expand Up @@ -116,6 +122,7 @@ authentication; [separate deployed security checks](docs/ONBOARDING.md#4-package

- **[docs/ONBOARDING.md](docs/ONBOARDING.md)** — customer setup / run / secure / deploy guide.
- **[docs/CONTRACT.md](docs/CONTRACT.md)** — the language-agnostic contract every implementation follows.
- **[setup/cyot/README.md](setup/cyot/README.md)** — standalone PowerShell stages, prerequisites and compatibility limits.

## Contributing a language or provider

Expand Down
35 changes: 30 additions & 5 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,28 @@ account and environment. Individual API contracts stay in the adapters.

### Setup script compatibility

The Preview 1 setup script creates the encryption-key secret, not the selected provider's API
credentials. Before live delivery, complete these steps:
The three [CYOT setup scripts](../setup/cyot/README.md) are separate stages for application registration,
resource provisioning and policy activation. Step 2 is a small launcher for its two companion ARM JSON
templates. The second template runs certificate/Graph/secret preparation in Azure under a separately
pre-authorized deployment identity. Copy its `arm` folder with the script; you do not run the
templates as separate manual steps. Steps 1 and 3 remain single-file scripts.
Pass the same application client ID between stages.

Supply the same customer `-TenantId` in each stage. Step 2 signs Azure CLI into that tenant and
validates the explicitly supplied `-SubscriptionId`. Resource names, the application ID and provider
settings are supplied through local parameter files. It never guesses from the previous CLI default.
See [running Step 2](../setup/cyot/README.md#run-step-2) and its deployment-identity prerequisites.

**Step 2 is not yet compatible with this sample unchanged.** It provisions outbound Entra OAuth
settings and sets the application's `tokenEncryptionKeyId`, whereas the implementations here use
provider API keys and require signed, unencrypted bearer tokens at Easy Auth. Importing the scripts
does not add outbound OAuth or access-token decryption to any runtime. Review the
[compatibility limits](../setup/cyot/README.md#compatibility-with-this-sample) before provisioning.
Step 3 separately refuses a policy write unless the live Graph schema exposes its exact CYOT contract.

Step 2 creates the encryption-key secret, not the selected provider's API credentials. For this
sample's API-key configuration, the following are still required; they do not resolve the OAuth or
access-token-encryption incompatibilities:

1. Set `KEY_VAULT_URL` to the vault containing the provider credentials. When it is the vault created
by setup, use that vault's `vaultUri`; otherwise explicitly select the credential vault and grant
Expand All @@ -36,7 +56,8 @@ credentials. Before live delivery, complete these steps:
4. Supply any additional options read by the selected adapter. Registering a provider does not make
every account option or channel automatically available.

The script already writes the correct `EPP_` names; no variable-prefix translation is required.
Shared settings already use the correct `EPP_` names; no variable-prefix translation is required.
Additional settings do not enable behavior the application does not implement.

| Setup value | Current application behavior |
|---|---|
Expand All @@ -48,6 +69,7 @@ The script already writes the correct `EPP_` names; no variable-prefix translati
| `EPP_DECRYPTION_KEY_PEM` | PEM or base64 PEM, usually resolved from a Key Vault secret reference. |
| `EPP_ENCRYPTION_KEY_ID` | Advisory mismatch warning only; not overlapping-key selection. |
| `EPP_EXPECTED_AUDIENCE`, `EPP_EXPECTED_ISSUER`, `EPP_EXPECTED_CLIENT_ID`, `EPP_TENANT_ID` | The script may write these, but this platform-authenticated application does not read them. The script's separate Easy Auth configuration enforces caller trust. |
| `EPP_PROVIDER_AUTH_MODE`, `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_SCOPE`, `EPP_OUTBOUND_CLIENT_ID`, `EPP_OUTBOUND_MI_CLIENT_ID` | Step 2 writes these for outbound OAuth. This sample does not consume them or perform the token exchange; it still requires the adapter's API-key secrets. |

**Do not use the script's `-NoEasyAuth` option with this application.** There is no application token
validator to take over. For the script's v1 registration, configure Easy Auth with the identifier URI
Expand All @@ -66,8 +88,11 @@ The script alone does not make this implementation conform to every Preview 1 re
- The guide requires voice digits to be spoken separately. This implementation preserves the supplied
message; verify the selected voice API's behavior rather than assuming unspaced digits are intelligible.

The pasted script also needs its advertised 100-byte UTF-8 endpoint-URL check before deployment.
A public-only certificate cannot supply the private key it later exports. Treat failed infrastructure
Step 2 also lacks the required 100-byte UTF-8 endpoint-URL check before deployment.
The Azure-hosted preparation requires an exportable RSA certificate and stores its private key in
Key Vault. ARM what-if/approval precedes each deployment, but does not preview individual
certificate or Graph operations inside deployment scripts. Code publishing and deployed endpoint
verification are separate. Treat failed infrastructure
role assignments as failures unless the exact assignment is verified as already present. Verify these
script prerequisites separately; the application tests do not validate provisioning.

Expand Down
126 changes: 126 additions & 0 deletions setup/cyot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# CYOT setup

| Stage | Responsibility |
|---|---|
| [Step 1](Step1-Register-CyotApplication.ps1) | Register or reuse the customer's multi-tenant application. Return its client ID for provider onboarding. |
| [Step 2](Step2-Setup-ExternalPhoneProvider.ps1) | A small PowerShell launcher that deploys both ARM JSON files. Certificate, Graph and secret preparation runs in Azure, not on the local machine. |
| [Step 3](Step3-Set-CyotPolicy.ps1) | Separately check the supported Graph policy contract and explicitly activate/update CYOT. Never called by Step 2. |

**Read [compatibility with this sample](#compatibility-with-this-sample) before deploying the application
code.** These templates preserve the imported provider/federation behavior; they do not implement
outbound OAuth in this repository's runtime.

## Files

```text
Step2-Setup-ExternalPhoneProvider.ps1
arm\
infrastructure.json
parameters.sample.json
function-config.json
function-config.parameters.sample.json
```

Keep the `arm` folder alongside Step 2. No other PowerShell file is needed.
Steps 1 and 3 remain individually copyable.

## Prerequisites

- PowerShell 7 and Azure CLI on the machine running Step 2. The local Step 2 launcher no longer needs
Microsoft Graph PowerShell modules or a Windows certificate store.
- An existing resource group and permission to deploy resources, role assignments, Azure Container
Instances and deployment scripts, plus permission to assign the deployment identity.
- A **separate, existing user-assigned preparation identity in the customer tenant**. An Entra
administrator must grant its required Microsoft Graph application permissions and ownership of
the Step 1 application beforehand. Prefer `Application.ReadWrite.OwnedBy` with ownership of the
target application where sufficient. Azure subscription Owner is not Microsoft Graph consent.
- Never attach that preparation identity to the running Function. The outbound Function identity
is a different identity, without application-administration privileges.
- Complete Step 1 and provider onboarding first. Use the same customer tenant, application ID,
resource names, plan and token version in both parameter files.

The infrastructure template grants the preparation identity Certificates Officer and Secrets Officer
on the selected vault. It does **not** grant Graph permissions. Deployment scripts use temporary
container/storage resources that incur charges until cleaned up. They need network access to Graph
and Key Vault; private-network environments need separate planning.

## Run Step 2

**Run only Step 2.** When both local parameter files are missing, a short inline prompt loop
collects their values and saves them before deploying:

```powershell
.\setup\cyot\Step2-Setup-ExternalPhoneProvider.ps1 `
-TenantId '<customer-tenant-id>' `
-SubscriptionId '<customer-subscription-id>' `
-ResourceGroup 'rg-external-phone-provider'
```

Step 2 reuses the supplied tenant and asks for the Function name, application ID, preparation
identity, plan/token version, provider metadata and location. Shared answers are written to both
files. Resource names are generated by ARM defaults, not by a PowerShell naming framework.
This does not create or authorize the preparation identity.

Subsequent runs reuse both existing JSON files without regenerating them. If only one file exists,
Step 2 stops rather than overwriting it. Custom `-InfrastructureParameters` and
`-ConfigurationParameters` paths are also supported; their parent directories must exist.

For existing deployments or advanced values, edit the local JSON files before running Step 2.
Use the original resource names instead of the new ARM defaults. Put shared name overrides in
both files, and choose token version 2 when the existing app issues v2 access tokens.
There is no automatic overwrite or regeneration. You can also copy and edit the sample JSON files.

Local parameter copies are ignored by Git. Never put private keys, access tokens or passwords in
the samples. Provider settings in `managedSettings` must be nonsecret; use Key Vault references for
credentials.

The launcher signs into the explicit tenant, verifies the subscription, then calls:

1. **[infrastructure.json](arm/infrastructure.json):** storage, hosting, Function, monitoring,
identities, vault, scoped role assignments and Easy Auth.
2. **[function-config.json](arm/function-config.json):** an Azure-hosted preparation script
creates/reuses an exportable RSA certificate in Key Vault, writes its PKCS8 private-key secret,
updates the app's public-key/endpoint metadata and federation, then applies app settings.

Both deployments are incremental and use ARM what-if confirmation. Failure stops the launcher.
What-if does not preview the individual Graph/data-plane changes inside a deployment script;
review that script and its permissions before approving deployment.

`setupRevision` can be incremented deliberately to rerun preparation. Valid existing certificates
and matching credentials are reused. An expired certificate requires deliberate renewal.
Outputs contain endpoint/application/key IDs, never private-key or secret values.
An existing private-key secret without its matching managed Key Vault certificate is not replaced:
import the matching certificate as `phone-provider-encryption` or start with a fresh vault.

**Function code publishing is now separate.** Build/publish the chosen language or compatible
package after configuration is ready. Step 2 no longer accepts `-ZipPath`, `-ZipUrl`, `-CertificatePath`
or `-EndpointUrl`; deployment settings belong in the JSON parameter files. Validate the deployed
endpoint before running Step 3.

## Existing resources

Use the original resource names. `infrastructure.json` still supports explicit `existing` snapshots
and role-assignment IDs for adopting earlier deployments, but the launcher no longer discovers or
migrates them automatically. Likewise, `existingAppSettings` preserves only settings you explicitly
provide. Review customized deployments before reuse: ARM incremental mode is not a property merge.
Prefer a dedicated resource group for an initial manual trial.

There is no resource cleanup, vault purge, recovery or group-wide tagging script. Steps 1 and 3
are unchanged.

## Compatibility with this sample

- The imported setup uses outbound Entra federation. The JavaScript, Python and .NET implementations
here still use provider API keys from Key Vault; these ARM changes do not add token exchange.
- Preparation retains the existing `tokenEncryptionKeyId` nomination. This conflicts with the
current sample's requirement for **unencrypted bearer tokens** at Easy Auth; resolve that
registration behavior before using the sample end to end.
- Easy Auth must remain enabled for the anonymous handlers. Do not use a custom-authentication
template path unless the deployed code implements its own validator.
- Flex Consumption uses managed-identity deployment storage. Premium uses an explicitly created
Azure Files share and a Key Vault reference for its content connection string. Verify the share
and reference are ready before publishing code.
- The sample waits for provider acceptance and uses one decryption key; setup does not add
asynchronous delivery, overlapping-key selection or retry handling.

See [the runtime contract](../../docs/CONTRACT.md) and [onboarding guide](../../docs/ONBOARDING.md).
Loading
Loading