From 2f9d821f2b1b289a7579d3c2ca1129d37710dda5 Mon Sep 17 00:00:00 2001 From: James Xian Date: Wed, 16 Sep 2026 11:25:49 -0700 Subject: [PATCH 1/3] Use ARM identity for EPP role assignments Derive the Azure RBAC operator from the selected subscription ARM access token's validated oid rather than Microsoft Graph /me. Track and display the Graph operator separately. Add optional -ForceAuthentication device-code sign-in for Azure CLI and Microsoft Graph without clearing shared caches or changing the default subscription. Reject combining forced authentication with noninteractive mode and document PrincipalNotFound recovery. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- setup/Setup-Epp.ps1 | 3 + setup/docs/README.md | 11 +++ setup/docs/Troubleshooting.md | 13 ++++ setup/support/Epp.Setup.psm1 | 141 ++++++++++++++++++++++++++-------- 4 files changed, 138 insertions(+), 30 deletions(-) diff --git a/setup/Setup-Epp.ps1 b/setup/Setup-Epp.ps1 index 6a1ce86..3a379a9 100644 --- a/setup/Setup-Epp.ps1 +++ b/setup/Setup-Epp.ps1 @@ -11,6 +11,8 @@ Public GitHub owner/repository containing the setup files. Use with SourceRef to test a fork. .PARAMETER InstallPrerequisites Install missing Microsoft Graph modules and the Azure CLI Bicep component after explicit opt-in. +.PARAMETER ForceAuthentication + Require fresh tenant-specific device-code sign-in for Azure CLI and Microsoft Graph. .EXAMPLE .\Setup-Epp.ps1 .EXAMPLE @@ -34,6 +36,7 @@ param( [string] $SourceRef = 'main', [switch] $NonInteractive, [switch] $InstallPrerequisites, + [switch] $ForceAuthentication, [switch] $ApproveDeployment ) diff --git a/setup/docs/README.md b/setup/docs/README.md index a583e87..5e9f7e0 100644 --- a/setup/docs/README.md +++ b/setup/docs/README.md @@ -101,6 +101,11 @@ consent includes broad app-role-management scopes because the approved deploymen `Application.Read.All` to the Microsoft phone-provider service principal. Authentication, module installation, Bicep installation, MFA, and consent prompts are not resource-creation approvals. +Use `-ForceAuthentication` when the machine has ambiguous cached identities. It requires interactive +device-code authentication for Azure CLI and Microsoft Graph, does not clear shared token caches, +and cannot be combined with `-NonInteractive`. Azure RBAC always uses the selected ARM token's +validated `oid`; Graph `/me` is tracked separately for application-management operations. + ## Step 2 - download and run one script Download and inspect [Setup-Epp.ps1](../Setup-Epp.ps1), or save it from the upstream raw URL: @@ -112,6 +117,12 @@ Invoke-WebRequest ` .\Setup-Epp.ps1 ``` +Force explicit account selection when testing on a shared or multi-account computer: + +```powershell +.\Setup-Epp.ps1 -ForceAuthentication +``` + The flow is: 1. **Collect missing customer inputs:** tenant, subscription, existing application client ID, Azure diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md index a51ae0b..3c58263 100644 --- a/setup/docs/Troubleshooting.md +++ b/setup/docs/Troubleshooting.md @@ -147,6 +147,19 @@ with these scopes and supply `-ApproveDeployment` separately. The tenant restriction uses Microsoft Graph beta `signInAudienceRestrictions`. If that preview is unavailable or the tenant policy blocks it, setup stops before mutation rather than silently allowing all organizational tenants. + +## PrincipalNotFound for the Azure operator + +Azure RBAC and Microsoft Graph can expose different object IDs for the same interactive account, +especially with brokered, guest, or aliased identities. Setup must not use Graph `/me` as an Azure +role-assignment principal. The current script decodes the selected subscription's ARM access token +in memory, validates its tenant, and passes its `oid` to Bicep. The token is never printed or saved. + +Use `-ForceAuthentication` to require fresh Azure CLI and Graph device-code sign-in when account +selection is ambiguous. This does not replace ARM-token identity selection and does not run +`az logout`, `az account clear`, or delete shared authentication caches. If a correct ARM `oid` +still receives `PrincipalNotFound`, wait for actual directory/RBAC replication and rerun with the +same prefix; retries must not substitute a Graph object ID. Use a distinct resource prefix for each language; setup rejects changing a previously tagged app to another runtime with the same prefix. diff --git a/setup/support/Epp.Setup.psm1 b/setup/support/Epp.Setup.psm1 index 652c344..512e104 100644 --- a/setup/support/Epp.Setup.psm1 +++ b/setup/support/Epp.Setup.psm1 @@ -399,19 +399,27 @@ function Initialize-EppBicep { } function Connect-EppAzureAccount { - param([hashtable] $Inputs, [switch] $NonInteractive) + param([hashtable] $Inputs, [switch] $NonInteractive, [switch] $ForceAuthentication) $account = $null - try { + if ($ForceAuthentication) { + if ($NonInteractive) { throw '-ForceAuthentication cannot be combined with -NonInteractive.' } + Write-Host "Reauthenticating Azure CLI to tenant $($Inputs.TenantId) with device code..." -ForegroundColor Cyan + Invoke-EppAz login --tenant $Inputs.TenantId --use-device-code --output none | Out-Null $account = Invoke-EppAz account show --subscription $Inputs.SubscriptionId --output json | ConvertFrom-Json } - catch { - if ($NonInteractive) { - throw "Azure CLI is not signed in to subscription '$($Inputs.SubscriptionId)' in tenant '$($Inputs.TenantId)'. Run az login first." + else { + try { + $account = Invoke-EppAz account show --subscription $Inputs.SubscriptionId --output json | ConvertFrom-Json + } + catch { + if ($NonInteractive) { + throw "Azure CLI is not signed in to subscription '$($Inputs.SubscriptionId)' in tenant '$($Inputs.TenantId)'. Run az login first." + } + Write-Host "Signing in to Azure tenant $($Inputs.TenantId)..." -ForegroundColor Cyan + Invoke-EppAz login --tenant $Inputs.TenantId --output none | Out-Null + $account = Invoke-EppAz account show --subscription $Inputs.SubscriptionId --output json | ConvertFrom-Json } - Write-Host "Signing in to Azure tenant $($Inputs.TenantId)..." -ForegroundColor Cyan - Invoke-EppAz login --tenant $Inputs.TenantId --output none | Out-Null - $account = Invoke-EppAz account show --subscription $Inputs.SubscriptionId --output json | ConvertFrom-Json } if ($account.id -ne $Inputs.SubscriptionId -or $account.tenantId -ne $Inputs.TenantId -or $account.state -ne 'Enabled' -or $account.environmentName -ne 'AzureCloud' -or $account.user.type -ne 'user') { @@ -420,6 +428,81 @@ function Connect-EppAzureAccount { return $account } +function ConvertFrom-EppJwtPayload { + param([string] $Token) + + $segments = $Token.Split('.') + if ($segments.Count -ne 3 -or $segments[1].Length -gt 65536) { + throw 'Azure CLI returned an invalid ARM access token.' + } + $payload = $segments[1].Replace('-', '+').Replace('_', '/') + switch ($payload.Length % 4) { + 2 { $payload += '==' } + 3 { $payload += '=' } + 1 { throw 'Azure CLI returned an invalid ARM access-token payload.' } + } + try { + return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | + ConvertFrom-Json -AsHashtable -ErrorAction Stop + } + catch { + throw 'Azure CLI returned an unreadable ARM access-token payload.' + } +} + +function Get-EppArmOperatorObjectId { + param([hashtable] $Inputs) + + $token = $null + try { + $token = Invoke-EppAz account get-access-token --subscription $Inputs.SubscriptionId ` + --resource 'https://management.azure.com/' --query accessToken --output tsv + if ([string]::IsNullOrWhiteSpace($token)) { throw 'Azure CLI did not return an ARM access token.' } + $claims = ConvertFrom-EppJwtPayload -Token $token + if ($claims['tid'] -ne $Inputs.TenantId) { + throw 'The ARM access token belongs to a different tenant than the approved deployment.' + } + return ConvertTo-EppGuid ([string]$claims['oid']) + } + finally { $token = $null } +} + +function Get-EppGraphOperator { + $operator = Invoke-MgGraphRequest -Method GET ` + -Uri 'https://graph.microsoft.com/v1.0/me?$select=id,userPrincipalName' ` + -OutputType PSObject -ErrorAction Stop + $id = ConvertTo-EppGuid ([string]$operator.id) + $account = if ([string]::IsNullOrWhiteSpace([string]$operator.userPrincipalName)) { $id } else { [string]$operator.userPrincipalName } + return [pscustomobject]@{ Id = $id; Account = $account } +} + +function Connect-EppGraphAccount { + param([hashtable] $Inputs, [switch] $NonInteractive, [switch] $ForceAuthentication) + + if ($NonInteractive -and $ForceAuthentication) { + throw '-ForceAuthentication requires interactive Graph device-code sign-in and cannot be combined with -NonInteractive.' + } + $graph = if ($ForceAuthentication) { $null } else { Get-EppInitialGraphContext } + if ($ForceAuthentication -or -not (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId)) { + $scopeList = $script:GraphRequiredScopes -join ', ' + if ($NonInteractive) { throw "Connect-MgGraph to the customer tenant with $scopeList before noninteractive setup." } + $connectArguments = @{ + TenantId = $Inputs.TenantId + Scopes = $script:GraphRequiredScopes + ContextScope = 'Process' + NoWelcome = $true + ErrorAction = 'Stop' + } + if ($ForceAuthentication) { $connectArguments.UseDeviceAuthentication = $true } + Connect-MgGraph @connectArguments + $graph = Get-MgContext -ErrorAction Stop + } + if (-not (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId)) { + throw "Microsoft Graph is not connected to the required customer tenant with delegated scopes: $($script:GraphRequiredScopes -join ', ')." + } + return [pscustomobject]@{ Context = $graph; Operator = Get-EppGraphOperator } +} + function Get-EppInitialGraphContext { try { return Get-MgContext -ErrorAction Stop } catch { @@ -645,9 +728,12 @@ function Initialize-EppResourceProviders { function Connect-EppContext { param( [hashtable] $Inputs, [Collections.IDictionary] $Names, - [switch] $NonInteractive, [switch] $InstallPrerequisites + [switch] $NonInteractive, [switch] $InstallPrerequisites, [switch] $ForceAuthentication ) + if ($NonInteractive -and $ForceAuthentication) { + throw '-ForceAuthentication requires interactive device-code sign-in and cannot be combined with -NonInteractive.' + } foreach ($command in @('az', 'New-SelfSignedCertificate', 'Export-Certificate')) { if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { if ($command -eq 'az') { @@ -662,20 +748,13 @@ function Connect-EppContext { } Initialize-EppBicep -NonInteractive:$NonInteractive -InstallPrerequisites:$InstallPrerequisites Import-EppGraphModules -NonInteractive:$NonInteractive -InstallPrerequisites:$InstallPrerequisites - $account = Connect-EppAzureAccount -Inputs $Inputs -NonInteractive:$NonInteractive - $operatorId = Invoke-EppAz rest --method get --url 'https://graph.microsoft.com/v1.0/me' ` - --subscription $Inputs.SubscriptionId --query id --output tsv - $operatorId = ConvertTo-EppGuid $operatorId - $graph = Get-EppInitialGraphContext - if (-not (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId)) { - $scopeList = $script:GraphRequiredScopes -join ', ' - if ($NonInteractive) { throw "Connect-MgGraph to the customer tenant with $scopeList before noninteractive setup." } - Connect-MgGraph -TenantId $Inputs.TenantId -Scopes $script:GraphRequiredScopes -ContextScope Process -NoWelcome -ErrorAction Stop - $graph = Get-MgContext -ErrorAction Stop - } - if (-not (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId)) { - throw "Microsoft Graph is not connected to the required customer tenant with delegated scopes: $($script:GraphRequiredScopes -join ', ')." - } + $account = Connect-EppAzureAccount -Inputs $Inputs -NonInteractive:$NonInteractive ` + -ForceAuthentication:$ForceAuthentication + $operatorId = Get-EppArmOperatorObjectId -Inputs $Inputs + $graphAccount = Connect-EppGraphAccount -Inputs $Inputs -NonInteractive:$NonInteractive ` + -ForceAuthentication:$ForceAuthentication + $graph = $graphAccount.Context + $graphOperator = $graphAccount.Operator $applications = @(Get-MgApplication -Filter "appId eq '$($Inputs.ApplicationId)'" -All -ErrorAction Stop) if ($applications.Count -ne 1) { throw 'Complete manual Step 1: exactly one existing application with this client ID is required.' } $application = Get-MgApplication -ApplicationId $applications[0].Id ` @@ -734,7 +813,8 @@ function Connect-EppContext { } } return [pscustomobject]@{ - OperatorId = $operatorId; GraphAccount = $graph.Account; Application = $application; TokenVersion = $version + OperatorId = $operatorId; GraphOperatorId = $graphOperator.Id; GraphAccount = $graphOperator.Account + Application = $application; TokenVersion = $version EndpointPrincipal = $endpointPrincipals | Select-Object -First 1 CallerPrincipal = $callerPrincipals | Select-Object -First 1 GraphPrincipal = $graphPrincipals[0] @@ -779,8 +859,8 @@ function Show-EppPlan { Write-Host 'Registration and regional readiness are checked before certificate/resource creation. Existing or in-progress registrations are reused.' Write-Host 'Includes the private packages blob container, Function system identity, Easy Auth, and diagnostic settings.' Write-Host 'System identity: Storage Blob Data Owner, Queue/Table Data Contributor, Key Vault Secrets User, Monitoring Metrics Publisher.' - Write-Host "Azure operator $($Context.OperatorId): Key Vault Secrets Officer and Storage Blob Data Contributor, scoped to these resources." - Write-Host "Graph operator $($Context.GraphAccount): configure the dedicated endpoint app and tenant service principals." + Write-Host "Azure operator $($Context.OperatorId) (ARM token oid): Key Vault Secrets Officer and Storage Blob Data Contributor." + Write-Host "Graph operator $($Context.GraphAccount) [$($Context.GraphOperatorId)]: configure the dedicated endpoint app and tenant service principals." if ($Context.Application.SignInAudience -ne 'AzureADMultipleOrgs') { Write-Host 'Change the dedicated endpoint application from single-tenant to organizational multi-tenant.' } @@ -1231,8 +1311,8 @@ function Invoke-EppDeployment { ) $graph = Get-MgContext - if (-not (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId) -or - $graph.Account -ne $Context.GraphAccount) { + $graphOperator = if (Test-EppGraphContext -Context $graph -TenantId $Inputs.TenantId) { Get-EppGraphOperator } else { $null } + if (-not $graphOperator -or $graphOperator.Id -ne $Context.GraphOperatorId) { throw 'The Graph session changed after the plan was reviewed. Rerun setup.' } # The single setup approval covers these planned writes, including SDK/certificate cmdlets. @@ -1368,7 +1448,8 @@ function Invoke-EppSetup { [string] $OutputDirectory, [string] $AssetDirectory, [string] $SourceBaseUri, [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$')] [string] $SourceRepository = 'Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample', - [switch] $NonInteractive, [switch] $InstallPrerequisites, [switch] $ApproveDeployment + [switch] $NonInteractive, [switch] $InstallPrerequisites, + [switch] $ForceAuthentication, [switch] $ApproveDeployment ) Write-Host 'Step 2: deploy the External Phone Provider endpoint. Steps 1 and 3 are manual.' -ForegroundColor Cyan @@ -1394,7 +1475,7 @@ function Invoke-EppSetup { Write-Host "`nChecking prerequisites and the selected Azure context (no resource changes)..." -ForegroundColor Cyan $context = Connect-EppContext -Inputs $inputs -Names $names -NonInteractive:$NonInteractive ` - -InstallPrerequisites:$InstallPrerequisites + -InstallPrerequisites:$InstallPrerequisites -ForceAuthentication:$ForceAuthentication $package = Get-EppPackage -Selection $selection -Directory $AssetDirectory $inputs.PackageSha256 = $package.Sha256 $inputs.SourcePackageSha256 = $package.SourceSha256 From 600893a66c45d3a33e73f2e0a26d8cfeba88821c Mon Sep 17 00:00:00 2001 From: James Xian Date: Wed, 16 Sep 2026 11:48:27 -0700 Subject: [PATCH 2/3] Request User.Read for EPP Graph operator lookup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- setup/docs/README.md | 9 ++++++--- setup/docs/Troubleshooting.md | 23 ++++++++++++++++++++++- setup/support/Epp.Setup.psm1 | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/setup/docs/README.md b/setup/docs/README.md index 5e9f7e0..b0cb168 100644 --- a/setup/docs/README.md +++ b/setup/docs/README.md @@ -65,8 +65,10 @@ disclosed outbound managed-identity federated credential. - An Azure **user** account permitted to deploy at subscription scope, create the listed resources, and create the scoped Azure role assignments. - A Microsoft Entra **Privileged Role Administrator** for granting the Microsoft first-party service - principal Graph `Application.Read.All`, plus delegated Graph scopes `Application.ReadWrite.All`, - `Application.Read.All`, and `AppRoleAssignment.ReadWrite.All`. + principal Graph `Application.Read.All`, plus delegated Graph scopes `User.Read`, + `Application.ReadWrite.All`, `Application.Read.All`, and `AppRoleAssignment.ReadWrite.All`. + `User.Read` is for the setup operator's `/me` lookup; it is not granted to the first-party service + principal or the endpoint app. - Microsoft Graph **beta** access for the Entra `signInAudienceRestrictions` allowed-tenants preview. The selected provider tenant is allowed in addition to the app's home tenant, which Entra always allows. - **Linux Premium EP1** available in the chosen region. Setup registers missing required Azure @@ -96,7 +98,8 @@ Setup normally detects these automatically. For unattended execution, allow inst Install Azure CLI through its official installation instructions if necessary. Setup checks the explicitly supplied subscription and tenant without changing the CLI's selected subscription. If no matching Azure user session exists, it runs `az login --tenant `. It separately requests -Graph sign-in before displaying the plan if the delegated session is missing required scopes. The +Graph sign-in before displaying the plan if the delegated session is missing required scopes, +including `User.Read` for operator identity readback. The consent includes broad app-role-management scopes because the approved deployment grants `Application.Read.All` to the Microsoft phone-provider service principal. Authentication, module installation, Bicep installation, MFA, and consent prompts are not resource-creation approvals. diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md index 3c58263..a38e9a4 100644 --- a/setup/docs/Troubleshooting.md +++ b/setup/docs/Troubleshooting.md @@ -139,7 +139,7 @@ setup makes it multi-tenant, restricts it to its home tenant plus the provider J through the Entra allowed-tenants preview, creates both required service principals, adds and assigns `Epp.Invoke`, and grants the Microsoft phone-provider service principal Graph `Application.Read.All`. -Graph needs delegated `Application.ReadWrite.All`, `Application.Read.All`, and +Graph needs delegated `User.Read`, `Application.ReadWrite.All`, `Application.Read.All`, and `AppRoleAssignment.ReadWrite.All`. Granting a Microsoft Graph application permission normally requires a Privileged Role Administrator. Noninteractive runs must authenticate both clients first with these scopes and supply `-ApproveDeployment` separately. @@ -148,6 +148,27 @@ The tenant restriction uses Microsoft Graph beta `signInAudienceRestrictions`. I unavailable or the tenant policy blocks it, setup stops before mutation rather than silently allowing all organizational tenants. +## Graph /me returns 403 Forbidden + +`GET /me?$select=id,userPrincipalName` requires delegated +[`User.Read`](https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0#permissions). +The application-management scopes do not authorize this profile lookup. Versions that added the +Graph operator readback without requesting `User.Read` could therefore fail during preflight. +This is a setup sign-in scope issue, not a request for another Azure or Entra administrator role. + +Use the updated script and source revision. It requests `User.Read` for the operator's Graph +PowerShell session and reconnects interactively when a cached session lacks it, before calling +`/me`. `-ForceAuthentication` also requests the complete scope set. Noninteractive runs must +authenticate first: + +```powershell +Connect-MgGraph -TenantId '' -ContextScope Process ` + -Scopes 'User.Read', 'Application.ReadWrite.All', 'Application.Read.All', 'AppRoleAssignment.ReadWrite.All' +``` + +This does not grant `User.Read` to the Microsoft phone-provider service principal or endpoint app. +Azure role assignments continue to use the ARM token's `oid`, not Graph `/me`. + ## PrincipalNotFound for the Azure operator Azure RBAC and Microsoft Graph can expose different object IDs for the same interactive account, diff --git a/setup/support/Epp.Setup.psm1 b/setup/support/Epp.Setup.psm1 index 512e104..fe10e2c 100644 --- a/setup/support/Epp.Setup.psm1 +++ b/setup/support/Epp.Setup.psm1 @@ -6,7 +6,7 @@ $script:MicrosoftGraphAppId = '00000003-0000-0000-c000-000000000000' $script:MicrosoftGraphApplicationReadAllRoleId = '9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30' $script:EppInvokeAppRoleId = 'ddf32018-9212-41c7-b73c-f5dfe73a2f24' $script:EppInvokeAppRoleValue = 'Epp.Invoke' -$script:GraphRequiredScopes = @('Application.ReadWrite.All', 'Application.Read.All', 'AppRoleAssignment.ReadWrite.All') +$script:GraphRequiredScopes = @('User.Read', 'Application.ReadWrite.All', 'Application.Read.All', 'AppRoleAssignment.ReadWrite.All') . (Join-Path $PSScriptRoot 'Epp.Packages.ps1') function Read-EppJson { From 30e1274062d5fd06caee1a7ecb4b355babbcb8c6 Mon Sep 17 00:00:00 2001 From: James Xian Date: Wed, 16 Sep 2026 13:06:36 -0700 Subject: [PATCH 3/3] Update Telesign CYOT endpoint Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- setup/providers/telesign.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup/providers/telesign.json b/setup/providers/telesign.json index ab47f65..190269e 100644 --- a/setup/providers/telesign.json +++ b/setup/providers/telesign.json @@ -12,13 +12,13 @@ "routes": { "sms": { "global": { - "endpoint": "https://rest-ww.telesign.com/integration/microsoft-cyot/sms", + "endpoint": "https://verify.telesign.com/integration/msft/cyot", "appId": "00000000-0000-0000-0000-000000000000", "timeoutMilliseconds": 1500, "retryIntervalSeconds": 30 }, "eu": { - "endpoint": "https://rest-ww.telesign.com/integration/microsoft-cyot/sms", + "endpoint": "https://verify.telesign.com/integration/msft/cyot", "appId": "00000000-0000-0000-0000-000000000000", "timeoutMilliseconds": 1500, "retryIntervalSeconds": 30 @@ -26,19 +26,19 @@ }, "voice": { "global": { - "endpoint": "https://rest-ww.telesign.com/integration/microsoft-cyot/voice", + "endpoint": "https://verify.telesign.com/integration/msft/cyot", "appId": "00000000-0000-0000-0000-000000000000", "timeoutMilliseconds": 1500, "retryIntervalSeconds": 30 }, "eu": { - "endpoint": "https://rest-ww.telesign.com/integration/microsoft-cyot/voice", + "endpoint": "https://verify.telesign.com/integration/msft/cyot", "appId": "00000000-0000-0000-0000-000000000000", "timeoutMilliseconds": 1500, "retryIntervalSeconds": 30 } } }, - "note": "The supplied global SMS and voice URLs are preserved. EU URLs and application IDs are explicit test values until Telesign provides them." + "note": "All Telesign channel and region selections use the supplied Microsoft CYOT integration endpoint. Application IDs remain explicit test values until Telesign provides them." } }