diff --git a/scripts/New-PublicClientApplication.ps1 b/scripts/New-PublicClientApplication.ps1 deleted file mode 100644 index 6fd2dd7..0000000 --- a/scripts/New-PublicClientApplication.ps1 +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env pwsh - -[CmdletBinding()] -param( - [Alias("n")] - [string]$AppName, - [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 " -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 (-not (Get-Command az -ErrorAction SilentlyContinue)) { - throw "Azure CLI 'az' is required." -} - -# 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)) { - $confirmation = Read-Host "Application '$AppName' already exists. Update it? [y/N]" - if ($confirmation -notmatch '^(?i:y|yes)$') { - Write-Host "Canceled." - exit 0 - } - $appId = $existingAppId -} else { - # 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 -$requiredResourceAccessFile = [System.IO.Path]::GetTempFileName() -Set-Content -Path $requiredResourceAccessFile -Value $requiredResourceAccess -NoNewline - -# Configure app to match "Azure Node Playground Public". -az ad app update ` - --id $appId ` - --sign-in-audience AzureADMyOrg ` - --is-fallback-public-client true ` - --required-resource-accesses "@$requiredResourceAccessFile" ` - --public-client-redirect-uris "http://localhost" "msal${appId}://auth" ` - --enable-access-token-issuance true ` - --enable-id-token-issuance true | Out-Null - -if ($LASTEXITCODE -ne 0) { - Remove-Item -Path $requiredResourceAccessFile -Force -ErrorAction SilentlyContinue - throw "Failed to configure application '$AppName'." -} -Remove-Item -Path $requiredResourceAccessFile -Force -ErrorAction SilentlyContinue - -# Azure CLI is used to grant admin consent. - -# Ensure service principal exists before granting tenant-wide admin consent. -az ad sp create --id $appId | Out-Null -if ($LASTEXITCODE -ne 0) { - throw "Failed to ensure service principal exists for '$AppName' ($appId)." -} - -# Grant admin consent for configured delegated permissions. -az ad app permission admin-consent --id $appId | Out-Null -if ($LASTEXITCODE -ne 0) { - throw "Failed to grant admin consent for '$AppName' ($appId)." -} - -$tenantId = az account show --query tenantId -o tsv -if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($tenantId)) { - throw "Failed to resolve tenantId from current Azure CLI context." -} - -if ([string]::IsNullOrWhiteSpace($existingAppId)) { - Write-Host "Created application '$AppName'" -} else { - Write-Host "Updated application '$AppName'" -} -Write-Host "appId: $appId" - -$configTemplate = @" -export const config = { - "appName": "$AppName", - "tenantId": "$tenantId", - "clientId": "$appId" -}; -"@ -Write-Output $configTemplate diff --git a/scripts/create-pca.js b/scripts/create-pca.js new file mode 100755 index 0000000..a265c29 --- /dev/null +++ b/scripts/create-pca.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import readline from "node:readline"; +import { spawnSync } from "node:child_process"; + +function runAz(args, options = {}) { + const result = spawnSync("az", args, { + encoding: "utf8", + stdio: options.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "pipe", "pipe"], + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0 && options.allowFailure !== true) { + throw new Error((result.stderr || "").trim() || `az ${args.join(" ")} failed`); + } + + return { + status: result.status ?? 1, + stdout: (result.stdout || "").trim(), + stderr: (result.stderr || "").trim(), + }; +} + +async function main() { + const argv = process.argv.slice(2); + const usageText = `Usage: ${path.basename(process.argv[1])} [options] +Options: + -n, --app-name Application display name (required) + -h, --help Show this help message and exit`; + let appName = ""; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "-h" || arg === "--help") { + console.log(usageText); + process.exit(0); + } + + if (arg === "-n" || arg === "--app-name") { + appName = argv[i + 1] || ""; + i += 1; + continue; + } + + if (arg.startsWith("-")) { + console.error(`Unknown option: ${arg}`); + console.error("Use -h or --help for usage information."); + process.exit(1); + } + + break; + } + + if (!appName) { + console.error("Error: Application name is required."); + console.log(usageText); + process.exit(1); + } + + let appId = runAz([ + "ad", + "app", + "list", + "--display-name", + appName, + "--query", + "[0].appId", + "-o", + "tsv", + ]).stdout; + + let userConfirmation = ""; + if (appId) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + userConfirmation = await new Promise((resolve) => { + rl.question( + `Application '${appName}' already exists. Update it? [y/N]: `, + (answer) => { + rl.close(); + resolve(answer.trim()); + }, + ); + }); + + if (!/^(yes|y)$/i.test(userConfirmation)) { + console.log("Canceled."); + process.exit(0); + } + } + + if (!appId) { + appId = runAz([ + "ad", + "app", + "create", + "--display-name", + appName, + "--query", + "appId", + "-o", + "tsv", + ]).stdout; + + if (!appId) { + console.error(`Error: Failed to create application '${appName}'.`); + process.exit(1); + } + } + + const requiredResourceAccess = [ + { + resourceAppId: "00000003-0000-0000-c000-000000000000", + resourceAccess: [{ id: "0e263e50-5827-48a4-b97c-d940288653c7", type: "Scope" }], + }, + { + resourceAppId: "499b84ac-1321-427f-aa17-267ca6975798", + resourceAccess: [{ id: "ee69721e-6c3a-468f-a9ec-302d16a4c599", type: "Scope" }], + }, + { + resourceAppId: "797f4846-ba00-4fd7-ba43-dac1f8f63013", + resourceAccess: [{ id: "41094075-9dad-400e-a0bd-54e686782033", type: "Scope" }], + }, + ]; + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pca-")); + const requiredResourceAccessFile = path.join(tempDir, "required-resource-accesses.json"); + + try { + fs.writeFileSync( + requiredResourceAccessFile, + JSON.stringify(requiredResourceAccess, null, 2), + "utf8", + ); + + try { + runAz( + [ + "ad", + "app", + "update", + "--id", + appId, + "--sign-in-audience", + "AzureADMyOrg", + "--is-fallback-public-client", + "true", + "--required-resource-accesses", + `@${requiredResourceAccessFile}`, + "--public-client-redirect-uris", + "http://localhost", + `msal${appId}://auth`, + "--enable-access-token-issuance", + "true", + "--enable-id-token-issuance", + "true", + ], + { quiet: true }, + ); + } catch { + console.error(`Error: Failed to configure application '${appName}' (${appId}).`); + process.exit(1); + } + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + + runAz(["ad", "sp", "create", "--id", appId], { quiet: true, allowFailure: true }); + + const adminConsentResult = runAz( + ["ad", "app", "permission", "admin-consent", "--id", appId], + { quiet: true, allowFailure: true }, + ); + if (adminConsentResult.status !== 0) { + console.error(`Error: Failed to grant admin consent for '${appName}' (${appId}).`); + process.exit(1); + } + + const tenantId = runAz(["account", "show", "--query", "tenantId", "-o", "tsv"]).stdout; + if (!tenantId) { + console.error("Error: Failed to resolve tenantId from current Azure CLI context."); + process.exit(1); + } + + if (userConfirmation) { + console.log(`Updated application '${appName}'`); + } else { + console.log(`Created application '${appName}'`); + } + console.log(`appId: ${appId}`); + console.log(`export const config = { + "appName": "${appName}", + "tenantId": "${tenantId}", + "clientId": "${appId}" +};`); +} + +main().catch((err) => { + console.error(`Error: ${err.message}`); + process.exit(1); +}); diff --git a/scripts/create-pca.sh b/scripts/create-pca.sh deleted file mode 100755 index 7ea9df4..0000000 --- a/scripts/create-pca.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -# Create the PCA for loggin in to Entra ID -function usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " -n, --app-name Application display name (required)" - echo " -h, --help Show this help message and exit" -} - -function main() { - local APP_NAME="" - while [[ $# -gt 0 ]]; do - case "$1" in - -h|--help) - usage - echo "Options:" - echo " -h, --help Show this help message and exit" - exit 0 - ;; - -n|--app-name) - APP_NAME="$2" - shift 2 - ;; - -*) - echo "Unknown option: $1" - echo "Use -h or --help for usage information." - exit 1 - ;; - *) # Leave the rest of the arguments for the script to process - break - ;; - esac - done - - if [[ -z "$APP_NAME" ]]; then - echo "Error: Application name is required." - usage - exit 1 - fi - - # Find the app by name - APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv) - if [[ -n "$APP_ID" ]]; then - local USER_CONFIRMATION - read -r -p "Application '$APP_NAME' already exists. Update it? [y/N]: " USER_CONFIRMATION - if [[ ! "$USER_CONFIRMATION" =~ ^([yY][eE][sS]|[yY])$ ]]; then - echo "Canceled." - exit 0 - fi - fi - - # Create the app when it does not already exist. - if [[ -z "$APP_ID" ]]; then - APP_ID=$(az ad app create --display-name "$APP_NAME" --query "appId" -o tsv) - if [[ -z "$APP_ID" ]]; then - echo "Error: Failed to create application '$APP_NAME'." - exit 1 - fi - fi - - local M365_GRAPH_APP_ID="00000003-0000-0000-c000-000000000000" - local M365_GRAPH_SCOPE_ID="0e263e50-5827-48a4-b97c-d940288653c7" - local AZURE_SERVICE_MGMT_APP_ID="797f4846-ba00-4fd7-ba43-dac1f8f63013" - local AZURE_SERVICE_MGMT_SCOPE_ID="41094075-9dad-400e-a0bd-54e686782033" - local AZURE_DEVOPS_APP_ID="499b84ac-1321-427f-aa17-267ca6975798" - local AZURE_DEVOPS_SCOPE_ID="ee69721e-6c3a-468f-a9ec-302d16a4c599" - - local REQUIRED_RESOURCE_ACCESS_JSON - REQUIRED_RESOURCE_ACCESS_JSON=$(cat < "$REQUIRED_RESOURCE_ACCESS_FILE" - - # Configure app to match "Azure Node Playground Public". - az ad app update \ - --id "$APP_ID" \ - --sign-in-audience AzureADMyOrg \ - --is-fallback-public-client true \ - --required-resource-accesses @"$REQUIRED_RESOURCE_ACCESS_FILE" \ - --public-client-redirect-uris "http://localhost" "msal${APP_ID}://auth" \ - --enable-access-token-issuance true \ - --enable-id-token-issuance true \ - 1>/dev/null - local UPDATE_EXIT_CODE=$? - rm -f "$REQUIRED_RESOURCE_ACCESS_FILE" - if [[ $UPDATE_EXIT_CODE -ne 0 ]]; then - echo "Error: Failed to configure application '$APP_NAME' ($APP_ID)." - exit 1 - fi - - # Ensure service principal exists before granting tenant-wide admin consent. - az ad sp create --id "$APP_ID" 1>/dev/null 2>/dev/null || true - - # Grant admin consent for configured delegated permissions. - az ad app permission admin-consent --id "$APP_ID" 1>/dev/null - if [[ $? -ne 0 ]]; then - echo "Error: Failed to grant admin consent for '$APP_NAME' ($APP_ID)." - exit 1 - fi - - local TENANT_ID - TENANT_ID=$(az account show --query tenantId -o tsv) - if [[ -z "$TENANT_ID" ]]; then - echo "Error: Failed to resolve tenantId from current Azure CLI context." - exit 1 - fi - - if [[ -z "$USER_CONFIRMATION" ]]; then - echo "Created application '$APP_NAME'" - else - echo "Updated application '$APP_NAME'" - fi - echo "appId: $APP_ID" - cat <" - exit 1 -fi - -APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv) -if [[ -z "$APP_ID" ]]; then - echo "Error: Application '$APP_NAME' not found." - exit 1 -fi - -TENANT_ID=$(az account show --query tenantId -o tsv) -if [[ -z "$TENANT_ID" ]]; then - echo "Error: Failed to resolve tenantId from current Azure CLI context." - exit 1 -fi - -az ad app show --id "$APP_ID" -o json -cat <