From a53d2896b15c77bd0a881f378e381f23bef6747c Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Fri, 6 Mar 2026 14:51:23 +0100 Subject: [PATCH] Add create and delete scripts creating Public Client Application (Remove unused JavaScript version). --- scripts/create-pca.js | 256 ------------------------------------------ scripts/create-pca.sh | 139 +++++++++++++++++++++++ scripts/delete-pca.sh | 44 ++++++++ 3 files changed, 183 insertions(+), 256 deletions(-) delete mode 100755 scripts/create-pca.js create mode 100755 scripts/create-pca.sh create mode 100755 scripts/delete-pca.sh diff --git a/scripts/create-pca.js b/scripts/create-pca.js deleted file mode 100755 index 01547ed..0000000 --- a/scripts/create-pca.js +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: MIT - - -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"; -import { parseArgs } from "node:util"; - -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 usageText = `Usage: ${path.basename(process.argv[1])} [options] -Options: - -c, --config Write JSON config to file (optional) - -h, --help Show this help message and exit`; - let values; - let positionals; - try { - ({ values, positionals } = parseArgs({ - args: process.argv.slice(2), - options: { - help: { type: "boolean", short: "h" }, - config: { type: "string", short: "c" }, - }, - strict: true, - allowPositionals: true, - })); - } catch (err) { - console.error(`Error: ${err.message}`); - console.error(usageText); - process.exit(1); - } - - if (values.help) { - console.log(usageText); - process.exit(0); - } - - if (positionals.length > 1) { - console.error( - "Error: Too many positional arguments. Only one app name positional argument is allowed.", - ); - console.error(usageText); - process.exit(1); - } - - const appName = positionals[0] || ""; - const configPath = values.config || ""; - - if (!appName) { - console.error("Error: Application name is required."); - console.error(usageText); - process.exit(1); - } - - let appId = runAz([ - "ad", - "app", - "list", - "--display-name", - appName, - "--query", - "[0].appId", - "-o", - "tsv", - ]).stdout; - - if (appId) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stderr, - }); - const answer = 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(answer)) { - console.error("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.warn( - `Warning: Failed to grant admin consent for '${appName}' (${appId}). Continuing without failing.`, - ); - if (adminConsentResult.stderr) { - console.warn(adminConsentResult.stderr); - } - } - - 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); - } - - const configTemplate = JSON.stringify( - { - tenantId, - clientId: appId, - }, - null, - 2, - ); - - if (configPath) { - const targetPath = path.resolve(configPath); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, `${configTemplate}\n`, "utf8"); - } - - console.log(configTemplate); -} - -main().catch((err) => { - console.error(`Error: ${err.message}`); - process.exit(1); -}); diff --git a/scripts/create-pca.sh b/scripts/create-pca.sh new file mode 100755 index 0000000..aa95a23 --- /dev/null +++ b/scripts/create-pca.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT + +set -uo pipefail + +usage() { + cat < +Options: + -c, --config Write JSON config to file (optional) + -h, --help Show this help message and exit +EOF +} + +CONFIG_PATH="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -c|--config) + if [[ $# -le 0 ]]; then + echo "Error: Missing value for --config" >&2 + exit 1 + fi + CONFIG_PATH="$2" + shift 2 + ;; + -*) + echo "Error: Unknown option: $1" >&2 + exit 1 + ;; + *) + break + ;; + esac +done + +APP_NAME="${1:-}" +if [[ -z "$APP_NAME" ]]; then + echo "Error: Application name is required." >&2 + usage >&2 + exit 1 +fi + +APP_ID="$(az ad app list --display-name "$APP_NAME" | jq -r '[.[].appId] | join(",")')" +if [[ "$APP_ID" =~ "," ]]; then + echo "Error: The application name '$APP_NAME' is not unique." >&2 + exit 1 +fi + +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'." >&2 + exit 1 + fi + echo "Created application '$APP_NAME' with appId '$APP_ID'." +else + printf "Application '%s' already exists. Update it? [y/N]: " "$APP_NAME" >&2 + read -r ANSWER + if [[ ! "$ANSWER" =~ ^[Yy]([Ee][Ss])*$ ]]; then + echo "Canceled." >&2 + exit 0 + fi +fi + +RESOURCE_ACCESS='[ + { + "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" } + ] + } +]' + +if ! az ad app update \ + --id "$APP_ID" \ + --sign-in-audience AzureADMyOrg \ + --is-fallback-public-client true \ + --required-resource-accesses "$RESOURCE_ACCESS" \ + --public-client-redirect-uris http://localhost "msal${APP_ID}://auth" \ + --enable-access-token-issuance true \ + --enable-id-token-issuance true \ + >/dev/null 2>&1; then + echo "Error: Failed to configure application '$APP_NAME' ($APP_ID)." >&2 + exit 1 +fi + +SP_ID="$(az ad sp show --id "$APP_ID" --query id -o tsv)" +if [[ -z "$SP_ID" ]]; then + SP_ID="$(az ad sp create --id "$APP_ID" --query id -o tsv)" + if [[ -z "$SP_ID" ]]; then + echo "Error: Failed to create service principal for application '$APP_NAME' ($APP_ID)." >&2 + exit 1 + fi +else + echo "Service principal for application '$APP_NAME' already exists with id '$SP_ID'." +fi + +az ad app permission admin-consent --id "$APP_ID" +if [[ $? -ne 0 ]]; then + echo "Error: Failed to grant admin consent for application '$APP_NAME' ($APP_ID)." >&2 + 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." >&2 + exit 1 +fi + +CONFIG="{ + \"tenantId\": \"$TENANT_ID\", + \"clientId\": \"$APP_ID\" +} +" + +if [[ -n "$CONFIG_PATH" ]]; then + mkdir -p "$(dirname "$CONFIG_PATH")" +else + CONFIG_PATH="/dev/null" +fi + +echo "$CONFIG" | tee "$CONFIG_PATH" diff --git a/scripts/delete-pca.sh b/scripts/delete-pca.sh new file mode 100755 index 0000000..bdbf920 --- /dev/null +++ b/scripts/delete-pca.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT + +set -uo pipefail + +APP_NAME="${1:-}" +if [[ -z "$APP_NAME" ]]; then + echo "Error: Application name is required." >&2 + echo "Usage: $(basename "$0") " >&2 + exit 1 +fi + +APP_ID="$(az ad app list --display-name "$APP_NAME" | jq -r '[.[].appId] | join(",")')" +if [[ "$APP_ID" =~ "," ]]; then + echo "Error: The application name '$APP_NAME' is not unique." >&2 + exit 1 +fi + +if [[ -z "$APP_ID" ]]; then + echo "Error: No application found with name '$APP_NAME'." >&2 + exit 1 +fi + +SP_ID="$(az ad sp show --id "$APP_ID" --query id -o tsv)" + +if [[ -z "$SP_ID" ]]; then + echo "No service principal found for application '$APP_NAME' ($APP_ID)." +fi + +# Get confirmation from user before deleting +read -p "Are you sure you want to delete application '$APP_NAME' with appId '$APP_ID' and its service principal? (y/N) " -n 1 -r +echo +if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then + echo "Aborting deletion." + exit 0 +fi + +if [[ -n "$SP_ID" ]]; then + az ad sp delete --id "$SP_ID" + echo "Deleted service principal with id '$SP_ID' for application '$APP_NAME' ($APP_ID)." +fi + +az ad app delete --id "$APP_ID" +echo "Deleted application '$APP_NAME' with appId '$APP_ID'."