Skip to content

Commit f6df177

Browse files
committed
fix(deploy): resolve auth + sample-bundle issues uncovered in e2e
configure_auth.sh / configure_auth.ps1: - Set globalValidation (requireAuthentication, unauthenticatedClientAction, redirectToProvider) directly in the authConfig PUT — the CLI flags were not reliably populating redirectToProvider, leaving the Web app responding 401 to browser users instead of redirecting to AAD. - Explicitly POST oauth2PermissionGrants to grant the API user_impersonation scope to the Web service principal. 'az ad app permission admin-consent' silently consents Microsoft Graph only and skips custom-API delegated scopes, which made MSAL acquireTokenSilent fail and rendered a blank SPA after successful login. - Override APP_WEB_AUTHORITY env var on the Web container app so MSAL.js uses a properly-formed authority URL. - Restart Web + API container revisions after secrets/env updates so the new values take effect without a manual restart. infra/main.bicep: - Drop redundant slash in APP_WEB_AUTHORITY composition; loginEndpoint already has a trailing slash, so '${loginEndpoint}/${tenantId}' produced a double-slash URL that broke MSAL. infra/scripts/post_deployment.sh: - Fix bash array iteration in Step 4b schema-id lookup. The previous 'for RID in $REGISTERED_IDS' de-references the array as a scalar (only the first element), causing only one file per sample bundle to upload. Switched to indexed iteration with ${!REGISTERED_IDS[@]} and a name lookup against REGISTERED_NAMES[$i].
1 parent e2a1a3e commit f6df177

4 files changed

Lines changed: 116 additions & 17 deletions

File tree

infra/main.bicep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.19.0' = {
11961196
}
11971197
{
11981198
name: 'APP_WEB_AUTHORITY'
1199-
value: '${environment().authentication.loginEndpoint}/${tenant().tenantId}'
1199+
value: '${environment().authentication.loginEndpoint}${tenant().tenantId}'
12001200
}
12011201
{
12021202
name: 'APP_WEB_SCOPE'

infra/scripts/configure_auth.ps1

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,33 @@ try {
195195
Write-Host " Or: https://login.microsoftonline.com/$TenantId/adminconsent?client_id=$WebClientId"
196196
}
197197

198+
# Belt-and-suspenders: explicitly grant the API user_impersonation scope to
199+
# the Web SP. `az ad app permission admin-consent` often skips custom-API
200+
# delegated permissions, leaving MSAL.js silent token acquisition broken
201+
# (which causes the SPA to render a blank page after sign-in).
202+
$WebSpId = az ad sp show --id $WebClientId --query id -o tsv 2>$null
203+
$ApiSpId = az ad sp show --id $ApiClientId --query id -o tsv 2>$null
204+
if ($WebSpId -and $ApiSpId) {
205+
$existing = az rest --method get `
206+
--url "https://graph.microsoft.com/v1.0/servicePrincipals/$WebSpId/oauth2PermissionGrants" `
207+
--query "value[?resourceId=='$ApiSpId'] | [0].id" -o tsv 2>$null
208+
if (-not $existing -or $existing -eq "null") {
209+
$body = "{`"clientId`":`"$WebSpId`",`"consentType`":`"AllPrincipals`",`"resourceId`":`"$ApiSpId`",`"scope`":`"user_impersonation`"}"
210+
try {
211+
az rest --method POST `
212+
--url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" `
213+
--headers "Content-Type=application/json" `
214+
--body $body --output none
215+
Write-Host " ✓ API user_impersonation scope granted to Web SP"
216+
} catch {
217+
Write-Host " ⚠️ Could not auto-grant API user_impersonation; SPA may show blank page until granted manually."
218+
$ConsentOk = $false
219+
}
220+
} else {
221+
Write-Host " ↺ API user_impersonation scope already granted"
222+
}
223+
}
224+
198225
# --- Step 4: Container App secrets ------------------------------------------
199226
Write-Host ""
200227
Write-Host "➡️ Step 4/6: Client secrets"
@@ -240,7 +267,7 @@ Write-Host ""
240267
Write-Host "➡️ Step 6/6: Wiring env vars and caller allowlist"
241268

242269
az containerapp update -n $WebName -g $ResourceGroup `
243-
--set-env-vars "APP_WEB_CLIENT_ID=$WebClientId" "APP_WEB_SCOPE=$WebScopeValue" "APP_API_SCOPE=$ApiScopeValue" "APP_AUTH_ENABLED=true" `
270+
--set-env-vars "APP_WEB_CLIENT_ID=$WebClientId" "APP_WEB_SCOPE=$WebScopeValue" "APP_API_SCOPE=$ApiScopeValue" "APP_WEB_AUTHORITY=https://login.microsoftonline.com/$TenantId" "APP_AUTH_ENABLED=true" `
244271
--output none
245272
Write-Host " ✓ Web env vars updated"
246273

@@ -262,6 +289,19 @@ function Patch-AuthConfig($CaName, $ClientId, $AddWebAllowed) {
262289
if ($AddWebAllowed -and ($allowed -notcontains $WebClientId)) { $allowed += $WebClientId }
263290
$policy.allowedApplications = $allowed
264291

292+
if (-not $current.properties.platform) { $current.properties | Add-Member -MemberType NoteProperty -Name platform -Value (@{}) }
293+
$current.properties.platform.enabled = $true
294+
if (-not $current.properties.globalValidation) { $current.properties | Add-Member -MemberType NoteProperty -Name globalValidation -Value ([pscustomobject]@{}) }
295+
$gv = $current.properties.globalValidation
296+
if ($gv.PSObject.Properties.Name -notcontains 'requireAuthentication') { $gv | Add-Member -MemberType NoteProperty -Name requireAuthentication -Value $true } else { $gv.requireAuthentication = $true }
297+
if ($AddWebAllowed) {
298+
if ($gv.PSObject.Properties.Name -notcontains 'unauthenticatedClientAction') { $gv | Add-Member -MemberType NoteProperty -Name unauthenticatedClientAction -Value 'Return401' } else { $gv.unauthenticatedClientAction = 'Return401' }
299+
if ($gv.PSObject.Properties.Name -contains 'redirectToProvider') { $gv.PSObject.Properties.Remove('redirectToProvider') }
300+
} else {
301+
if ($gv.PSObject.Properties.Name -notcontains 'unauthenticatedClientAction') { $gv | Add-Member -MemberType NoteProperty -Name unauthenticatedClientAction -Value 'RedirectToLoginPage' } else { $gv.unauthenticatedClientAction = 'RedirectToLoginPage' }
302+
if ($gv.PSObject.Properties.Name -notcontains 'redirectToProvider') { $gv | Add-Member -MemberType NoteProperty -Name redirectToProvider -Value 'azureactivedirectory' } else { $gv.redirectToProvider = 'azureactivedirectory' }
303+
}
304+
265305
$tmp = New-TemporaryFile
266306
$current | ConvertTo-Json -Depth 20 | Out-File -FilePath $tmp -Encoding utf8
267307
Retry { az rest --method put --url $url --headers "Content-Type=application/json" --body "@$tmp" | Out-Null }
@@ -272,10 +312,20 @@ Patch-AuthConfig $ApiName $ApiClientId $true
272312
Patch-AuthConfig $WebName $WebClientId $false
273313
Write-Host " ✓ authConfigs normalized (issuer, audiences, allowedApplications)"
274314

275-
az containerapp auth update -n $WebName -g $ResourceGroup --unauthenticated-client-action RedirectToLoginPage --output none
276-
az containerapp auth update -n $ApiName -g $ResourceGroup --unauthenticated-client-action Return401 --output none
277315
Write-Host " ✓ Unauthenticated requests: Web → login, API → 401"
278316

317+
# Restart active revisions so containers pick up newly-set client secrets.
318+
# (`az containerapp secret set` does NOT trigger a new revision on its own.)
319+
function Restart-ActiveRevision($CaName) {
320+
$rev = az containerapp revision list -n $CaName -g $ResourceGroup --query "[?properties.active] | [0].name" -o tsv 2>$null
321+
if ($rev -and $rev -ne "null") {
322+
az containerapp revision restart -n $CaName -g $ResourceGroup --revision $rev --output none 2>$null
323+
}
324+
}
325+
Restart-ActiveRevision $WebName
326+
Restart-ActiveRevision $ApiName
327+
Write-Host " ✓ Restarted Web + API container revisions to apply secrets"
328+
279329
Write-Host ""
280330
Write-Host "============================================================"
281331
Write-Host "🔐 Auth configuration complete."

infra/scripts/configure_auth.sh

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,33 @@ else
245245
echo " ✓ Admin consent granted"
246246
fi
247247

248+
# Belt-and-suspenders: explicitly grant the API scope to the Web SP.
249+
# `az ad app permission admin-consent` is unreliable for app-to-app delegated
250+
# permissions exposed by a freshly-created custom API — the consent often only
251+
# covers Microsoft Graph permissions and silently skips the API. Without the
252+
# API grant, MSAL.js acquireTokenSilent() fails on the SPA and the page is blank.
253+
WEB_SP_ID="$(az ad sp show --id "$WEB_CLIENT_ID" --query id -o tsv 2>/dev/null || true)"
254+
API_SP_ID="$(az ad sp show --id "$API_CLIENT_ID" --query id -o tsv 2>/dev/null || true)"
255+
if [[ -n "$WEB_SP_ID" && -n "$API_SP_ID" ]]; then
256+
EXISTING_GRANT="$(az rest --method get \
257+
--url "https://graph.microsoft.com/v1.0/servicePrincipals/${WEB_SP_ID}/oauth2PermissionGrants" \
258+
--query "value[?resourceId=='${API_SP_ID}'] | [0].id" -o tsv 2>/dev/null || true)"
259+
if [[ -z "$EXISTING_GRANT" || "$EXISTING_GRANT" == "null" ]]; then
260+
if az rest --method POST \
261+
--url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" \
262+
--headers "Content-Type=application/json" \
263+
--body "{\"clientId\":\"${WEB_SP_ID}\",\"consentType\":\"AllPrincipals\",\"resourceId\":\"${API_SP_ID}\",\"scope\":\"user_impersonation\"}" \
264+
--output none 2>/dev/null; then
265+
echo " ✓ API user_impersonation scope granted to Web SP"
266+
else
267+
echo " ⚠️ Could not auto-grant API user_impersonation; SPA may show blank page until granted manually."
268+
CONSENT_OK=false
269+
fi
270+
else
271+
echo " ↺ API user_impersonation scope already granted"
272+
fi
273+
fi
274+
248275
# -----------------------------------------------------------------------------
249276
# Step 4: Client secrets + Container App secrets
250277
# -----------------------------------------------------------------------------
@@ -314,29 +341,34 @@ echo ""
314341
echo "➡️ Step 6/6: Wiring env vars and caller allowlist"
315342

316343
# Update Web container env vars (other values left untouched)
344+
# Also overwrite APP_WEB_AUTHORITY to fix a pre-existing bicep bug that produces
345+
# a malformed authority URL (double slash before tenant id).
317346
az containerapp update -n "$WEB_NAME" -g "$RESOURCE_GROUP" \
318347
--set-env-vars \
319348
"APP_WEB_CLIENT_ID=$WEB_CLIENT_ID" \
320349
"APP_WEB_SCOPE=$WEB_SCOPE_VALUE" \
321350
"APP_API_SCOPE=$API_SCOPE_VALUE" \
351+
"APP_WEB_AUTHORITY=https://login.microsoftonline.com/$TENANT_ID" \
322352
"APP_AUTH_ENABLED=true" \
323353
--output none
324-
echo " ✓ Web env vars: APP_WEB_CLIENT_ID / APP_WEB_SCOPE / APP_API_SCOPE / APP_AUTH_ENABLED"
354+
echo " ✓ Web env vars: APP_WEB_CLIENT_ID / APP_WEB_SCOPE / APP_API_SCOPE / APP_WEB_AUTHORITY / APP_AUTH_ENABLED"
325355

326356
# Patch both authConfigs:
327357
# - API: add Web client id to allowedApplications
328358
# - Both: reset allowedAudiences to only the clientId, normalize openIdIssuer
329359
patch_authconfig() {
330360
local ca_name="$1"
331361
local client_id="$2"
332-
local add_web_allowed="$3" # "true" / "false"
362+
local add_web_allowed="$3" # "true" (API side) / "false" (Web side)
333363
local url="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.App/containerApps/${ca_name}/authConfigs/current?api-version=2024-03-01"
334364
local cur patched
335365
cur="$(az rest --method get --url "$url")"
336366
patched="$(echo "$cur" | ADD_WEB="$add_web_allowed" WEB_CLIENT_ID="$WEB_CLIENT_ID" CLIENT_ID="$client_id" TENANT_ID="$TENANT_ID" python3 -c "
337367
import json, os, sys
338368
d = json.load(sys.stdin)
339369
props = d.setdefault('properties', {})
370+
props['platform'] = props.get('platform') or {}
371+
props['platform']['enabled'] = True
340372
idp = props.setdefault('identityProviders', {})
341373
aad = idp.setdefault('azureActiveDirectory', {})
342374
reg = aad.setdefault('registration', {})
@@ -348,6 +380,14 @@ allowed = set(policy.get('allowedApplications') or [])
348380
if os.environ['ADD_WEB'] == 'true':
349381
allowed.add(os.environ['WEB_CLIENT_ID'])
350382
policy['allowedApplications'] = sorted(allowed)
383+
gv = props.setdefault('globalValidation', {})
384+
gv['requireAuthentication'] = True
385+
if os.environ['ADD_WEB'] == 'true':
386+
gv['unauthenticatedClientAction'] = 'Return401'
387+
gv.pop('redirectToProvider', None)
388+
else:
389+
gv['unauthenticatedClientAction'] = 'RedirectToLoginPage'
390+
gv['redirectToProvider'] = 'azureactivedirectory'
351391
print(json.dumps(d))
352392
")"
353393
echo "$patched" > /tmp/authconfig_patch.json
@@ -361,13 +401,25 @@ patch_authconfig "$API_NAME" "$API_CLIENT_ID" "true"
361401
patch_authconfig "$WEB_NAME" "$WEB_CLIENT_ID" "false"
362402
echo " ✓ authConfigs normalized (issuer, audiences, allowedApplications)"
363403

364-
# Final lockdown
365-
az containerapp auth update -n "$WEB_NAME" -g "$RESOURCE_GROUP" \
366-
--unauthenticated-client-action RedirectToLoginPage --output none
367-
az containerapp auth update -n "$API_NAME" -g "$RESOURCE_GROUP" \
368-
--unauthenticated-client-action Return401 --output none
404+
# Final lockdown handled in patch_authconfig globalValidation above.
369405
echo " ✓ Unauthenticated requests: Web → login, API → 401"
370406

407+
# Restart active revisions so containers pick up newly-set client secrets.
408+
# (`az containerapp secret set` does NOT trigger a new revision on its own.)
409+
restart_active_revision() {
410+
local ca_name="$1"
411+
local rev
412+
rev="$(az containerapp revision list -n "$ca_name" -g "$RESOURCE_GROUP" \
413+
--query "[?properties.active] | [0].name" -o tsv 2>/dev/null || true)"
414+
if [[ -n "$rev" && "$rev" != "null" ]]; then
415+
az containerapp revision restart -n "$ca_name" -g "$RESOURCE_GROUP" \
416+
--revision "$rev" --output none 2>/dev/null || true
417+
fi
418+
}
419+
restart_active_revision "$WEB_NAME"
420+
restart_active_revision "$API_NAME"
421+
echo " ✓ Restarted Web + API container revisions to apply secrets"
422+
371423
echo ""
372424
echo "============================================================"
373425
echo "🔐 Auth configuration complete."

infra/scripts/post_deployment.sh

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,12 +297,9 @@ else
297297

298298
# Look up schema ID from registered schemas
299299
SCHEMA_ID=""
300-
RIDX=0
301-
for RID in $REGISTERED_IDS; do
302-
RIDX=$((RIDX + 1))
303-
RNAME=$(echo "$REGISTERED_NAMES" | tr ' ' '\n' | sed -n "${RIDX}p")
304-
if [ "$RNAME" = "$SCHEMA_CLASS" ]; then
305-
SCHEMA_ID="$RID"
300+
for i in "${!REGISTERED_IDS[@]}"; do
301+
if [ "${REGISTERED_NAMES[$i]}" = "$SCHEMA_CLASS" ]; then
302+
SCHEMA_ID="${REGISTERED_IDS[$i]}"
306303
break
307304
fi
308305
done

0 commit comments

Comments
 (0)