The PIMActivation Portal can be deployed into your own Azure tenant in a single operation using the included Bicep template. This page is the long-form walkthrough; the README has the short version. > **Source of truth:** [`Portal/deploy/bicep/portal-selfhosted.bicep`](https://github.com/Noble-Effeciency13/PIMActivation-Portal/blob/main/Portal/deploy/bicep/portal-selfhosted.bicep). [`Portal/deploy/azuredeploy.json`](https://github.com/Noble-Effeciency13/PIMActivation-Portal/blob/main/Portal/deploy/azuredeploy.json) is auto-generated by the **Sync Deployment Templates** workflow. ## What you get - An Azure Static Web App serving the portal SPA. - A user-assigned managed identity scoped narrowly as **Website Contributor** on the SWA only (so the deployment script can read the SWA deployment token via `listSecrets()` — no broader resource-group access). - A customer-owned storage account that caches the downloaded portal source archive for fallback redeploys. - A deployment script that downloads, configures, and deploys the portal in one shot, and attempts to add the generated SPA redirect URIs to your Entra app registration. ## Prerequisites 1. **An Azure subscription** and a resource group you can deploy into. 2. **An existing single-tenant Entra ID SPA app registration** for the portal. See [[App Registration Setup|App-Registration-Setup]]. Azure deployment scripts cannot complete interactive sign-in to Microsoft Graph reliably, so the app registration must exist before deployment and its application (client) ID is a required parameter. 3. **Tenant-wide admin consent** for the delegated permissions if your tenant requires it. The deployment outputs an `adminConsentUrl` you can use after deployment. ## One-click deployment Click **Deploy to Azure** in the [README](https://github.com/Noble-Effeciency13/PIMActivation-Portal#self-hosted-azure-deployment) — or use the URL directly: ```text https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FNoble-Effeciency13%2FPIMActivation-Portal%2Fmain%2FPortal%2Fdeploy%2Fazuredeploy.json ``` You will be prompted for the parameters below. ## Parameter reference | Parameter | Required | Default | Description | |---|---|---|---| | `applicationClientId` | yes | — | Application (client) ID of an existing single-tenant Entra ID SPA app registration. | | `tenantId` | no | `subscription().tenantId` | Tenant ID. Defaults to the current subscription tenant. | | `customDomain` | no | `''` | Optional custom domain (e.g. `pim.contoso.com`). Included in the redirect URI auto-merge attempt. The Static Web App custom domain must still be bound manually after DNS validates. | | `portalSourceBranch` | no | `main` | Branch to download. Each deployment pulls the latest commit at deployment time. | | `portalSourceArchiveUrl` | no | `''` | Override the source download with a publicly reachable ZIP. The archive must contain `Portal/index.html`. | | `deploymentScriptRunId` | no | `utcNow('yyyyMMddHHmmss')` | Forces the deployment script to rerun and gives each run a fresh resource name to avoid Azure Files sharing violations on retry. | | `location` | no | `resourceGroup().location` | Azure region. | | `staticWebAppSku` | no | `Free` | `Free` or `Standard`. | | `resourceTag` | no | `PIMActivation` | Tag applied to all created resources. | ## What the deployment script does 1. Downloads the portal source ZIP from `portalSourceBranch` (or your `portalSourceArchiveUrl`). 2. Caches the archive in the customer-owned storage account for fallback redeploys. 3. Locates `Portal/index.html` inside the extracted archive. 4. **Injects** your `applicationClientId` and `tenantId` into `Portal/js/msal-config.js` via `sed` replacement of the `__PORTAL_CLIENT_ID__` and `__PORTAL_TENANT_ID__` placeholders. 5. Verifies that no placeholders remain after injection (fails the deployment if any are left). 6. Copies `index.html`, `staticwebapp.config.json`, and the `css`, `js`, `images` folders into a deployment payload directory. 7. Reads the SWA deployment token via `az staticwebapp secrets list`. 8. Installs the SWA CLI (`@azure/static-web-apps-cli`) and uploads the payload. 9. Attempts to merge the generated default hostname (and `customDomain`, if supplied) into the app registration's SPA redirect URIs through Microsoft Graph. ## Outputs | Output | Use | |---|---| | `portalUrl` | URL of the deployed Static Web App. | | `staticWebAppName` | Name of the SWA resource. | | `redirectUris` | SPA redirect URIs to add to the app registration if the script could not auto-add them. | | `clientId` | Echoes the client ID you supplied. | | `customDomainNextStep` | If you supplied `customDomain`, the CNAME and binding instructions. | | `adminConsentUrl` | Tenant-wide admin consent URL for the app registration. | | `sourceArchiveCache` | The customer-owned blob that holds the cached source archive. | | `nextStep` | A reminder to verify redirect URIs and grant admin consent if needed. | ## After the first deployment 1. **Open `portalUrl`.** You should see the portal sign-in screen. 2. **Verify redirect URIs.** If the deployment log shows a Microsoft Graph permission warning, copy the `redirectUris` output and add them under your app registration's **Authentication** blade → **Single-page application** platform. 3. **Grant admin consent** if your tenant requires it, using the `adminConsentUrl` output. 4. **(Optional) Bind your custom domain.** If you supplied `customDomain`, follow `customDomainNextStep`: create a CNAME pointing your domain at the SWA default hostname, then add and validate the custom domain on the Static Web App resource. ## Reruns and redeployment The deployment script uses a fresh resource name on each run (driven by `deploymentScriptRunId`), which sidesteps the Azure Files sharing violations that can otherwise block retries. Reruns therefore pick up the newest commit on `portalSourceBranch` automatically. If the GitHub branch download fails and `portalSourceArchiveUrl` was not supplied, the script falls back to the cached source archive in the customer-owned storage account. ## Deploying from a private fork If you fork the repository privately, GitHub returns `404` to the unauthenticated deployment script. To deploy from a private fork, build a ZIP of the repository (PowerShell `Compress-Archive` works) and upload it somewhere publicly reachable — for example a public Azure Blob with a short-lived SAS URL — then pass that URL as `portalSourceArchiveUrl`. The archive must contain `Portal/index.html` somewhere inside it. ## Troubleshooting ### The deployment finishes but the portal renders a blank shell Check the deployment script logs for `placeholder injection failed`. If the placeholders were not replaced, the SPA can't construct a valid MSAL config. Re-run the deployment with the same parameters. ### Microsoft Graph permission warning in the deployment log The deployment identity could not update the app registration. Add the `redirectUris` output manually under **Authentication** → **Single-page application** in your app registration. ### `403` on activation requests after deployment Almost always one of: - The user does not actually have a PIM-eligible assignment for the role. - The tenant requires admin consent for one of the delegated scopes — use the `adminConsentUrl` output. - An Conditional Access policy requires an auth context the portal hasn't been told about (open the role's policy in Entra and check **Activation requirements**). ### Custom domain redirect mismatch after binding After binding the custom domain on the Static Web App, make sure that domain is also listed under your app registration's SPA redirect URIs. The deployment script attempted to add it; if the attempt failed, add it manually. ### "AADSTS500113: No reply address is registered for the application" The redirect URI was not added to the app registration. Add the value of the `redirectUris` output (or the deployed SWA URL with no trailing slash) under **Authentication** → **Single-page application**. ### Browser caches the old favicon after a sync Browser favicon caches are sticky. Open in a private window or clear site data to validate. See also [[Troubleshooting|Troubleshooting]] for runtime issues.