From f66e5985f7dd003ac68fa377ce22f636762d2a9a Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sat, 7 Feb 2026 12:46:43 +0100 Subject: [PATCH] Add PublicClientApplication script --- scripts/New-PublicClientApplication.ps1 | 194 ++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 scripts/New-PublicClientApplication.ps1 diff --git a/scripts/New-PublicClientApplication.ps1 b/scripts/New-PublicClientApplication.ps1 new file mode 100644 index 0000000..1620c05 --- /dev/null +++ b/scripts/New-PublicClientApplication.ps1 @@ -0,0 +1,194 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [Alias("n")] + [string]$AppName, + [switch]$UsePowershellModules, + [Alias("h")] + [switch]$Help +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Show-Usage { + Write-Host "Usage: ./New-PublicClientApplication.ps1 -AppName " + Write-Host "Options:" + Write-Host " -AppName, -n Application display name (required)" + Write-Host " -UsePowershellModules Use Az.Accounts/Az.Resources cmdlets instead of Azure CLI" + Write-Host " -Help, -h Show this help message and exit" +} + +function Get-RequiredResourceAccess { + param( + [Parameter(Mandatory = $true)] + [string]$M365GraphAppId, + [Parameter(Mandatory = $true)] + [string]$M365GraphScopeId, + [Parameter(Mandatory = $true)] + [string]$AzureDevOpsAppId, + [Parameter(Mandatory = $true)] + [string]$AzureDevOpsScopeId, + [Parameter(Mandatory = $true)] + [string]$AzureServiceMgmtAppId, + [Parameter(Mandatory = $true)] + [string]$AzureServiceMgmtScopeId + ) + + return @( + @{ + resourceAppId = $M365GraphAppId + resourceAccess = @( + @{ + id = $M365GraphScopeId + type = "Scope" + } + ) + }, + @{ + resourceAppId = $AzureDevOpsAppId + resourceAccess = @( + @{ + id = $AzureDevOpsScopeId + type = "Scope" + } + ) + }, + @{ + resourceAppId = $AzureServiceMgmtAppId + resourceAccess = @( + @{ + id = $AzureServiceMgmtScopeId + type = "Scope" + } + ) + } + ) +} + +if ($Help) { + Show-Usage + exit 0 +} + +if ([string]::IsNullOrWhiteSpace($AppName)) { + Write-Error "Application name is required." + Show-Usage + exit 1 +} + +$m365GraphAppId = "00000003-0000-0000-c000-000000000000" +$m365GraphScopeId = "0e263e50-5827-48a4-b97c-d940288653c7" +$azureServiceMgmtAppId = "797f4846-ba00-4fd7-ba43-dac1f8f63013" +$azureServiceMgmtScopeId = "41094075-9dad-400e-a0bd-54e686782033" +$azureDevOpsAppId = "499b84ac-1321-427f-aa17-267ca6975798" +$azureDevOpsScopeId = "ee69721e-6c3a-468f-a9ec-302d16a4c599" + +if ($UsePowershellModules) { + if (-not (Get-Command Get-AzADApplication -ErrorAction SilentlyContinue)) { + throw "Get-AzADApplication cmdlet not found. Install Az.Resources." + } + if (-not (Get-Command New-AzADApplication -ErrorAction SilentlyContinue)) { + throw "New-AzADApplication cmdlet not found. Install Az.Resources." + } + if (-not (Get-Command Update-AzADApplication -ErrorAction SilentlyContinue)) { + throw "Update-AzADApplication cmdlet not found. Install Az.Resources." + } + + $azContext = Get-AzContext + if ($null -eq $azContext) { + throw "No Azure context found. Run Connect-AzAccount first." + } + + $existingApp = Get-AzADApplication -DisplayName $AppName -First 1 + if ($null -ne $existingApp) { + Write-Error "Application '$AppName' already exists." + exit 1 + } + + $requiredResourceAccess = Get-RequiredResourceAccess ` + -M365GraphAppId $m365GraphAppId ` + -M365GraphScopeId $m365GraphScopeId ` + -AzureDevOpsAppId $azureDevOpsAppId ` + -AzureDevOpsScopeId $azureDevOpsScopeId ` + -AzureServiceMgmtAppId $azureServiceMgmtAppId ` + -AzureServiceMgmtScopeId $azureServiceMgmtScopeId + + $webConfig = @{ + implicitGrantSettings = @{ + enableAccessTokenIssuance = $true + enableIdTokenIssuance = $true + } + } + + # Create first to obtain appId needed for msal://auth redirect URI. + $newApp = New-AzADApplication ` + -DisplayName $AppName ` + -SignInAudience "AzureADMyOrg" ` + -IsFallbackPublicClient ` + -PublicClientRedirectUri @("http://localhost") ` + -RequiredResourceAccess $requiredResourceAccess ` + -Web $webConfig + + if ($null -eq $newApp -or [string]::IsNullOrWhiteSpace($newApp.AppId)) { + throw "Failed to create application '$AppName' via Az.Resources." + } + + $appId = $newApp.AppId + + Update-AzADApplication ` + -ApplicationId $appId ` + -SignInAudience "AzureADMyOrg" ` + -IsFallbackPublicClient ` + -RequiredResourceAccess $requiredResourceAccess ` + -PublicClientRedirectUri @("http://localhost", "msal${appId}://auth") ` + -Web $webConfig | Out-Null +} else { + # Find the app by name + $existingAppId = az ad app list --display-name $AppName --query "[0].appId" -o tsv + if ($LASTEXITCODE -ne 0) { + throw "Failed to query existing applications." + } + if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { + Write-Error "Application '$AppName' already exists." + exit 1 + } + + # Create the app + $appId = az ad app create --display-name $AppName --query "appId" -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($appId)) { + throw "Failed to create application '$AppName'." + } + + $requiredResourceAccess = Get-RequiredResourceAccess ` + -M365GraphAppId $m365GraphAppId ` + -M365GraphScopeId $m365GraphScopeId ` + -AzureDevOpsAppId $azureDevOpsAppId ` + -AzureDevOpsScopeId $azureDevOpsScopeId ` + -AzureServiceMgmtAppId $azureServiceMgmtAppId ` + -AzureServiceMgmtScopeId $azureServiceMgmtScopeId | ConvertTo-Json -Depth 10 -Compress + + $publicClientRedirectUris = @( + "http://localhost", + "msal${appId}://auth" + ) | ConvertTo-Json -Compress + + # Configure app to match "Azure Node Playground Public". + az ad app update ` + --id $appId ` + --set ` + "signInAudience=AzureADMyOrg" ` + "isFallbackPublicClient=true" ` + "requiredResourceAccess=$requiredResourceAccess" ` + "publicClient.redirectUris=$publicClientRedirectUris" ` + "web.implicitGrantSettings.enableAccessTokenIssuance=true" ` + "web.implicitGrantSettings.enableIdTokenIssuance=true" | Out-Null + + if ($LASTEXITCODE -ne 0) { + throw "Failed to configure application '$AppName'." + } +} + +Write-Host "Created application '$AppName'" +Write-Host "appId: $appId"