Added JavaScript version of Create PCA script, and removed platfrom specific Bash and PowerShell ones.
This commit is contained in:
@@ -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 <name>"
|
||||
Write-Host "Options:"
|
||||
Write-Host " -AppName, -n <name> 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
|
||||
209
scripts/create-pca.js
Executable file
209
scripts/create-pca.js
Executable file
@@ -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 <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);
|
||||
});
|
||||
@@ -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 <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 <<EOF
|
||||
[
|
||||
{
|
||||
"resourceAppId": "${M365_GRAPH_APP_ID}",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "${M365_GRAPH_SCOPE_ID}",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceAppId": "${AZURE_DEVOPS_APP_ID}",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "${AZURE_DEVOPS_SCOPE_ID}",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceAppId": "${AZURE_SERVICE_MGMT_APP_ID}",
|
||||
"resourceAccess": [
|
||||
{
|
||||
"id": "${AZURE_SERVICE_MGMT_SCOPE_ID}",
|
||||
"type": "Scope"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
EOF
|
||||
)
|
||||
|
||||
local REQUIRED_RESOURCE_ACCESS_FILE
|
||||
REQUIRED_RESOURCE_ACCESS_FILE=$(mktemp)
|
||||
echo "$REQUIRED_RESOURCE_ACCESS_JSON" > "$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 <<EOF
|
||||
export const config = {
|
||||
"appName": "$APP_NAME",
|
||||
"tenantId": "$TENANT_ID",
|
||||
"clientId": "$APP_ID"
|
||||
};
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
APP_NAME="$1"
|
||||
|
||||
if [[ -z "$APP_NAME" ]]; then
|
||||
echo "Usage: $0 <app-name>"
|
||||
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 <<EOF
|
||||
export const config = {
|
||||
"appName": "$APP_NAME",
|
||||
"tenantId": "$TENANT_ID",
|
||||
"clientId": "$APP_ID"
|
||||
};
|
||||
EOF
|
||||
Reference in New Issue
Block a user