Create-CopilotWebhookApp.ps1
<#
.SYNOPSIS Creates a Microsoft Entra App Registration and adds a Federated Identity Credential (FIC) for Copilot Studio's security webhook. The script uses MSAL.PS for authentication and is compatible with PowerShell 5.1+. .PARAMETER TenantId The Azure AD tenant ID (GUID format required). .PARAMETER Endpoint Webhook endpoint URL. Must be a valid HTTPS URL that exactly matches what Copilot Studio uses. .PARAMETER DisplayName Display name for the new app registration. Must be unique within the tenant. .PARAMETER FICName Name of the Federated Identity Credential to create. .PARAMETER DryRun Optional. When specified, shows what would be created without making actual changes. .EXAMPLE .\Create-CopilotWebhookApp.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -Endpoint "https://your.webhook/endpoint" -DisplayName "My Copilot App" -FICName "WebhookFIC" .EXAMPLE .\Create-CopilotWebhookApp.ps1 -TenantId "12345678-1234-1234-1234-123456789012" -Endpoint "https://your.webhook/endpoint" -DisplayName "My Copilot App" -FICName "WebhookFIC" -DryRun .NOTES Requires PowerShell 5.1 or later and internet connectivity to access Microsoft Graph API. The script will automatically install MSAL.PS module if not present. The user must have sufficient permissions to create app registrations in the specified tenant. #> <#PSScriptInfo .VERSION 1.0.0 .GUID 8a028c75-549a-4194-aaa7-56c5d5b1f30c .AUTHOR Avital Livshits .COMPANYNAME Microsoft .COPYRIGHT (c) 2025 Microsoft. All rights reserved. .TAGS Copilot Studio, Security Webhooks, Security Extensibility .LICENSEURI https://opensource.org/licenses/MIT .EXTERNALMODULEDEPENDENCIES MSAL.PS .DESCRIPTION This script creates a Microsoft Entra App Registration and adds a Federated Identity Credential (FIC) for Copilot Studio's security webhook. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "Enter the Azure AD tenant ID (GUID)")] [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')] [string]$TenantId, [Parameter(Mandatory = $true, HelpMessage = "Enter the webhook endpoint URL (must be HTTPS)")] [ValidatePattern('^https://.*')] [string]$Endpoint, [Parameter(Mandatory = $true, HelpMessage = "Enter a display name for the app registration")] [ValidateLength(1, 120)] [string]$DisplayName, [Parameter(Mandatory = $true, HelpMessage = "Enter a name for the Federated Identity Credential")] [ValidateLength(1, 120)] [string]$FICName, [Parameter(Mandatory = $false, HelpMessage = "Preview what would be created without making changes")] [switch]$DryRun ) # Global variables $script:AccessToken = $null $script:GraphBaseUrl = "https://graph.microsoft.com/v1.0" #region Helper Functions function Write-Step { param([string]$Message, [string]$Color = "Cyan") Write-Host "`n$Message" -ForegroundColor $Color } function Write-Success { param([string]$Message) Write-Host "✔ $Message" -ForegroundColor Green } function Write-Error { param([string]$Message) Write-Host "✗ $Message" -ForegroundColor Red } function Write-Warning { param([string]$Message) Write-Host "⚠ $Message" -ForegroundColor Yellow } function Show-MSALInstallationInstructions { <# .SYNOPSIS Displays manual installation instructions for MSAL.PS module. #> Write-Host "`nManual Installation Required:" -ForegroundColor Yellow Write-Host " 1. Open a new PowerShell window" -ForegroundColor White Write-Host " 2. Run the following command:" -ForegroundColor White Write-Host " Install-Module -Name MSAL.PS -Scope CurrentUser -Force" -ForegroundColor Cyan Write-Host " 3. If prompted to trust the repository, type 'Y' and press Enter" -ForegroundColor White Write-Host " 4. After installation completes, run this script again" -ForegroundColor White Write-Host "`nIf you encounter issues:" -ForegroundColor Yellow Write-Host " • Ensure you have internet connectivity" -ForegroundColor Gray Write-Host " • Check if your organization uses a proxy server" -ForegroundColor Gray Write-Host " • Contact your IT administrator if access is blocked" -ForegroundColor Gray } function Test-MSALModule { <# .SYNOPSIS Checks if MSAL.PS module is installed and installs it if needed. #> try { Write-Step "Checking MSAL.PS module..." # Check if MSAL.PS is installed $msalModule = Get-Module -ListAvailable -Name MSAL.PS if (-not $msalModule) { Write-Warning "MSAL.PS module is not installed" # Check if we can install modules (not in restricted execution policy) $executionPolicy = Get-ExecutionPolicy if ($executionPolicy -eq "Restricted") { Write-Warning "PowerShell execution policy is Restricted - cannot auto-install modules" Show-MSALInstallationInstructions throw "MSAL.PS module is required but cannot be auto-installed due to execution policy." } # Ask user for permission to install Write-Host " The MSAL.PS module is required for authentication." -ForegroundColor Yellow Write-Host " This module will be installed for the current user only." -ForegroundColor Yellow if ($DryRun) { Write-Host " [DRY RUN] Would install MSAL.PS module" -ForegroundColor Gray return $true } $response = Read-Host " Install MSAL.PS module now? [Y/N]" if ($response -notmatch '^[Yy]') { Show-MSALInstallationInstructions throw "MSAL.PS module is required to continue." } Write-Host " Installing MSAL.PS module..." -ForegroundColor Gray try { Install-Module -Name MSAL.PS -Scope CurrentUser -Force -AllowClobber Write-Success "MSAL.PS module installed successfully" } catch { Write-Error "Failed to install MSAL.PS module automatically: $($_.Exception.Message)" Show-MSALInstallationInstructions throw "Cannot continue without MSAL.PS module. Please install it manually and try again." } } else { Write-Success "MSAL.PS module is available" } # Import the module if (-not $DryRun) { Import-Module MSAL.PS -Force Write-Host " MSAL.PS module loaded successfully" -ForegroundColor Gray } return $true } catch { throw "MSAL.PS module setup failed: $($_.Exception.Message)" } } function Get-AccessToken { <# .SYNOPSIS Authenticates with Microsoft Graph using MSAL and returns an access token. #> param([string]$TenantId) try { Write-Step "Authenticating with Microsoft Graph..." $clientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # Microsoft Graph Command Line Tools $scopes = @("https://graph.microsoft.com/Application.ReadWrite.All", "https://graph.microsoft.com/AppRoleAssignment.ReadWrite.All") # Use MSAL to get token with device code flow Write-Host " Starting device code authentication..." -ForegroundColor Gray Write-Host " You will need to complete authentication in your browser." -ForegroundColor Yellow $tokenResult = Get-MsalToken -ClientId $clientId -TenantId $TenantId -Scopes $scopes -DeviceCode if ($tokenResult -and $tokenResult.AccessToken) { Write-Success "Authenticated successfully using MSAL" return $tokenResult.AccessToken } else { throw "Failed to obtain access token" } } catch { throw "Failed to authenticate: $($_.Exception.Message)" } } function Invoke-GraphRequest { <# .SYNOPSIS Makes a REST request to Microsoft Graph with proper error handling. #> param( [string]$Uri, [string]$Method = "GET", [hashtable]$Body = $null, [string]$ContentType = "application/json" ) if (-not $script:AccessToken) { throw "No access token available. Please authenticate first." } $headers = @{ Authorization = "Bearer $script:AccessToken" "Content-Type" = $ContentType } try { $params = @{ Uri = $Uri Method = $Method Headers = $headers } if ($Body -and $Method -ne "GET") { if ($ContentType -eq "application/json") { $params.Body = ($Body | ConvertTo-Json -Depth 10) } else { $params.Body = $Body } } return Invoke-RestMethod @params } catch { $errorDetails = "Unknown error" if ($_.Exception.Response) { try { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $responseBody = $reader.ReadToEnd() $errorObj = $responseBody | ConvertFrom-Json if ($errorObj.error) { $errorDetails = "$($errorObj.error.code): $($errorObj.error.message)" } } catch { $errorDetails = "HTTP $($_.Exception.Response.StatusCode)" } } throw "Graph API request failed: $errorDetails" } } function Test-ApplicationExists { <# .SYNOPSIS Checks if an application with the given display name already exists and returns app details if found. #> param([string]$DisplayName) $encodedName = [System.Web.HttpUtility]::UrlEncode($DisplayName) $uri = "$script:GraphBaseUrl/applications?`$filter=displayName eq '$encodedName'" $response = Invoke-GraphRequest -Uri $uri -Method GET if ($response.value -and $response.value.Count -gt 0) { return $response.value[0] # Return the first matching app } return $null } function Test-FederatedIdentityCredentialExists { <# .SYNOPSIS Checks if a Federated Identity Credential with the given name already exists on the app. #> param( [string]$ApplicationObjectId, [string]$FICName ) try { $uri = "$script:GraphBaseUrl/applications/$ApplicationObjectId/federatedIdentityCredentials" $response = Invoke-GraphRequest -Uri $uri -Method GET foreach ($fic in $response.value) { if ($fic.name -eq $FICName) { return $true } } return $false } catch { # If we can't fetch FICs, assume it doesn't exist and let the creation attempt handle any errors return $false } } function New-ApplicationRegistration { <# .SYNOPSIS Creates a new application registration. #> param([string]$DisplayName) $appBody = @{ displayName = $DisplayName signInAudience = "AzureADMyOrg" } $uri = "$script:GraphBaseUrl/applications" return Invoke-GraphRequest -Uri $uri -Method POST -Body $appBody } function New-ServicePrincipalForApp { <# .SYNOPSIS Creates a service principal for the given application. #> param([string]$AppId) $spBody = @{ appId = $AppId } $uri = "$script:GraphBaseUrl/servicePrincipals" return Invoke-GraphRequest -Uri $uri -Method POST -Body $spBody } function New-FederatedIdentityCredential { <# .SYNOPSIS Creates a Federated Identity Credential for the application. #> param( [string]$ApplicationObjectId, [string]$FICName, [string]$TenantId, [string]$Endpoint ) # Build the subject claim as required by Copilot Studio $issuer = "https://login.microsoftonline.com/$TenantId/v2.0" $audience = "api://AzureADTokenExchange" # Encode tenant ID and endpoint for the subject $encodedTenantId = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($TenantId)).TrimEnd('=').Replace('+', '-').Replace('/', '_') $encodedEndpoint = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Endpoint)).TrimEnd('=').Replace('+', '-').Replace('/', '_') $subject = "/eid1/c/pub/t/$encodedTenantId/a/m1WPnYRZpEaQKq1Cceg--g/$encodedEndpoint" $ficBody = @{ name = $FICName issuer = $issuer subject = $subject description = "Federated Identity Credential for Copilot Studio webhook authentication" audiences = @($audience) } $uri = "$script:GraphBaseUrl/applications/$ApplicationObjectId/federatedIdentityCredentials" return Invoke-GraphRequest -Uri $uri -Method POST -Body $ficBody } #endregion #region Main Logic function Start-AppCreation { param( [string]$TenantId, [string]$Endpoint, [string]$DisplayName, [string]$FICName, [bool]$DryRun ) try { Write-Host "========================================" -ForegroundColor Cyan Write-Host " Copilot Webhook App Registration Tool" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan if ($DryRun) { Write-Warning "DRY RUN MODE - No changes will be made" } Write-Host "`nConfiguration:" -ForegroundColor White Write-Host " Tenant ID: $TenantId" -ForegroundColor Gray Write-Host " Endpoint: $Endpoint" -ForegroundColor Gray Write-Host " App Name: $DisplayName" -ForegroundColor Gray Write-Host " FIC Name: $FICName" -ForegroundColor Gray # Step 1: Ensure MSAL.PS is available Test-MSALModule # Step 2: Authenticate if (-not $DryRun) { $script:AccessToken = Get-AccessToken -TenantId $TenantId } else { Write-Step "Would authenticate with Microsoft Graph using MSAL" } # Step 3: Check if app already exists Write-Step "Checking if application already exists..." $app = $null $sp = $null $useExistingApp = $false if (-not $DryRun) { $existingApp = Test-ApplicationExists -DisplayName $DisplayName if ($existingApp) { Write-Warning "Application '$DisplayName' already exists" Write-Host " App ID: $($existingApp.appId)" -ForegroundColor Gray Write-Host " Object ID: $($existingApp.id)" -ForegroundColor Gray $response = Read-Host "`n Would you like to add a new FIC to the existing app? [Y/N]" if ($response -match '^[Yy]') { $useExistingApp = $true $app = $existingApp Write-Success "Will use existing application" # Check if FIC name already exists Write-Step "Checking if FIC name is unique..." $ficExists = Test-FederatedIdentityCredentialExists -ApplicationObjectId $app.id -FICName $FICName if ($ficExists) { throw "Federated Identity Credential '$FICName' already exists on this application. Please choose a different FIC name." } Write-Success "FIC name is unique" } else { throw "Operation cancelled. Please choose a different application name or remove the existing app." } } else { Write-Success "Application name is available" } } else { Write-Host " Would check if '$DisplayName' already exists" -ForegroundColor Gray Write-Host " Would prompt user if app exists" -ForegroundColor Gray } # Step 4: Create application (if needed) if (-not $useExistingApp) { Write-Step "Creating application registration..." if (-not $DryRun) { $app = New-ApplicationRegistration -DisplayName $DisplayName Write-Success "Application created successfully" Write-Host " App ID: $($app.appId)" -ForegroundColor Gray Write-Host " Object ID: $($app.id)" -ForegroundColor Gray } else { Write-Host " Would create application '$DisplayName'" -ForegroundColor Gray $app = @{ appId = "00000000-0000-0000-0000-000000000000"; id = "mock-object-id" } } # Step 5: Create service principal Write-Step "Creating service principal..." if (-not $DryRun) { $sp = New-ServicePrincipalForApp -AppId $app.appId Write-Success "Service principal created successfully" Write-Host " Service Principal ID: $($sp.id)" -ForegroundColor Gray } else { Write-Host " Would create service principal for app" -ForegroundColor Gray $sp = @{ id = "mock-sp-id" } } } else { Write-Step "Using existing application - skipping service principal creation" # For existing apps, we assume the service principal already exists # We could optionally verify this, but it's not critical for FIC creation $sp = @{ id = "existing-sp-id" } } # Step 6: Create Federated Identity Credential Write-Step "Creating Federated Identity Credential..." if (-not $DryRun) { $fic = New-FederatedIdentityCredential -ApplicationObjectId $app.id -FICName $FICName -TenantId $TenantId -Endpoint $Endpoint Write-Success "Federated Identity Credential created successfully" } else { Write-Host " Would create FIC '$FICName' with:" -ForegroundColor Gray Write-Host " Issuer: https://login.microsoftonline.com/$TenantId/v2.0" -ForegroundColor Gray Write-Host " Audience: api://AzureADTokenExchange" -ForegroundColor Gray Write-Host " Subject: /eid1/c/pub/t/[encoded-tenant]/a/m1WPnYRZpEaQKq1Cceg--g/[encoded-endpoint]" -ForegroundColor Gray } # Step 7: Summary Write-Host "`n========================================" -ForegroundColor Green if ($DryRun) { Write-Host " DRY RUN COMPLETE" -ForegroundColor Green } else { if ($useExistingApp) { Write-Host " FIC ADDED TO EXISTING APP" -ForegroundColor Green } else { Write-Host " APPLICATION REGISTRATION COMPLETE" -ForegroundColor Green } } Write-Host "========================================" -ForegroundColor Green Write-Host "`nApplication Details:" -ForegroundColor White Write-Host " Display Name: $DisplayName" -ForegroundColor Gray Write-Host " App ID: $($app.appId)" -ForegroundColor Gray Write-Host " Object ID: $($app.id)" -ForegroundColor Gray Write-Host " Service Principal ID: $($sp.id)" -ForegroundColor Gray Write-Host " FIC Name: $FICName" -ForegroundColor Gray Write-Host "`n✔ Process completed successfully!" -ForegroundColor Green } catch { Write-Error "Failed to create application: $($_.Exception.Message)" Write-Host "`nTroubleshooting:" -ForegroundColor White Write-Host " • Ensure you have Application.ReadWrite.All permissions in the tenant" -ForegroundColor Yellow Write-Host " • Verify the tenant ID is correct" -ForegroundColor Yellow Write-Host " • Check that the application name is unique" -ForegroundColor Yellow Write-Host " • Ensure the endpoint URL is valid and uses HTTPS" -ForegroundColor Yellow Write-Host " • Check that the FIC name is unique within the application" -ForegroundColor Yellow throw } } #endregion # Main execution try { # Load System.Web for URL encoding (available in PowerShell 5.1) Add-Type -AssemblyName System.Web Start-AppCreation -TenantId $TenantId -Endpoint $Endpoint -DisplayName $DisplayName -FICName $FICName -DryRun $DryRun.IsPresent } catch { Write-Host "`n❌ Script execution failed!" -ForegroundColor Red exit 1 } |