This sample provides a minimal Single Page Application (Angular 20, standalone component) and a backend .NET 8 Web API protected by Azure AD B2C using a custom policy whose PolicyId is now B2C_1A_SignUpSignIn (renamed from the earlier B2C_1_susi to avoid colliding with an existing built‑in user flow of the same name). The underlying custom policy files live in policies/ and introduce REST API orchestration, error handling and localization.
NOTE: This is a lightweight hand-written scaffold (not full Angular CLI output) to keep the repository concise. You can migrate it into a standard Angular CLI workspace later if needed.
- Node.js 18+ (for frontend tooling)
- .NET 8 SDK
- Azure AD B2C tenant
- Two App Registrations in B2C:
- SPA (Public client)
- API (Protected resource)
| Term | Meaning |
|---|---|
| Application (client) ID | GUID identifying an app registration |
| Application ID URI | Logical resource identifier used as token audience (e.g. https://tenant.onmicrosoft.com/api) |
| Scope | Permission segment appended to Application ID URI (e.g. /user.read) |
- Azure AD B2C tenant → App registrations → New registration → Name:
contoso-transit-api. - Leave redirect URI empty (not needed for APIs).
- After creation → Expose an API → Set Application ID URI (choose one style, be consistent):
- Recommended domain style:
https://<tenant>.onmicrosoft.com/<API_CLIENT_ID> - OR GUID style:
api://<API_CLIENT_ID>
- Add a scope:
- Name:
user.read - Admin consent display name:
Read user profile - Description:
Allows reading the signed-in user's profile. - State: Enabled
- (Optional) Add additional scopes or app roles later.
- New registration → Name:
contoso-transit-spa. - Platform: Single-page application → Redirect URI:
http://localhost:4200. - Enable Access tokens & ID tokens (implicit + auth code flow support for SPA via PKCE).
- API permissions → Add a permission → My APIs → pick
contoso-transit-api→ selectuser.read→ Add. - Grant admin consent (avoids user consent prompts in many B2C scenarios).
The active policy set now lives under policies/LocalAccounts/ (the previous versioned samples in policies/NA/ are retained only for comparison). The SignUpSignIn policy XML uses PolicyId B2C_1A_SignUpSignIn. If you previously configured apps against a built‑in user flow B2C_1_susi, update their authority URLs to reference this custom policy id.
- In the Azure Portal open your B2C tenant.
- Create (or verify) the two required application registrations for custom policies (if they do not already exist):
IdentityExperienceFramework(web, reply URL:https://jwt.ms, allow public client = No)ProxyIdentityExperienceFramework(native/public client, redirect URI:myapp://authorhttps://jwt.ms)- Grant the
IdentityExperienceFrameworkapplication delegated permissions toProxyIdentityExperienceFramework(API permissions → Add → APIs my organization uses).
These two special apps let Azure AD B2C run your custom policy orchestration (IEF = server side; Proxy = acts as the public/native client brokering tokens). The most common confusion is exposing an API on the Proxy app so the IEF app can request its delegated permission.
- Register
IdentityExperienceFramework(IEF)
- Azure AD B2C Portal → App registrations → New registration.
- Name:
IdentityExperienceFramework. - Supported account types: Accounts in this organizational directory only.
- Redirect URI (web):
https://jwt.ms(temporary; any HTTPS you control would work, jwt.ms is convenient for token inspection). - Leave SPA / mobile unchecked here.
- After creation: Authentication blade → ensure "Allow public client flows" is Disabled.
- No need to expose an API for this app.
- Register
ProxyIdentityExperienceFramework(Proxy IEF)
- New registration.
- Name:
ProxyIdentityExperienceFramework. - Supported account types: Accounts in this directory only.
- Redirect URI: (Public client / mobile & desktop) add
myapp://auth(placeholder) AND optionally addhttps://jwt.msfor convenience. - After creation: Authentication blade → ensure "Allow public client flows" is Enabled (since it's a public/native client).
- Expose the Proxy API (critical step)
- Open the
ProxyIdentityExperienceFrameworkapp → Expose an API. - Click "Set" for Application ID URI if not already set; use the default suggested value (e.g.
api://<proxy-client-id>). You may also choose domain style:https://<tenant>.onmicrosoft.com/proxyidentityexperienceframework, either works—just be consistent. - Under Scopes defined by this API → Add a scope:
- Scope name:
user_impersonation(conventionally used; name is arbitrary but must match what you later select). - Admin consent display name:
Access ProxyIdentityExperienceFramework. - Admin consent description:
Allows the IdentityExperienceFramework app to call the proxy to execute custom policies. - State: Enabled → Add scope.
- Scope name:
- (Optional) Pre-authorize IEF for the Proxy scope
- The Azure AD B2C portal UI periodically changes. Some tenants show an "Authorized client applications" section under Expose an API; others do not.
- If you DO see it: Add a client application → paste the
IdentityExperienceFrameworkapp (client) ID → tickuser_impersonation→ Add. - If you DO NOT see that section: Pre-authorization is optional. You can rely solely on admin consent in the next step. If you still want to pre-authorize, open the
ProxyIdentityExperienceFrameworkapp → Manifest, and add the IEF app client ID to theknownClientApplicationsarray, e.g.:Save the manifest. (If the property already exists, append the ID instead of replacing existing entries.)"knownClientApplications": [ "<IDENTITY_EXPERIENCE_FRAMEWORK_CLIENT_ID>" ]
- Grant delegated permission in IEF app
- Open the
IdentityExperienceFrameworkapp → API permissions → Add a permission → My APIs → selectProxyIdentityExperienceFramework→ check theuser_impersonationscope → Add permissions. - Click "Grant admin consent" for the tenant to avoid runtime consent prompts.
- Verify
- In
IdentityExperienceFramework→ API permissions you should now seeProxyIdentityExperienceFramework/user_impersonationwith a green granted check. - In
ProxyIdentityExperienceFramework→ Expose an API you should see the scope and the IEF app listed under Authorized client applications.
Common pitfalls:
- Forgetting to create the scope on the Proxy app (results in "The provided application '' is not configured to allow the '.../user_impersonation' scope").
- Not granting admin consent (interactive consent prompts appear during policy execution, sometimes leading to cryptic errors).
- Mis-typing the Application ID URI; if you change it later, re-run admin consent.
- Mixing up client IDs in policy XML (if your policy XML includes references—this simplified sample omits those explicit IDs, but full starter packs usually require you to replace placeholders).
You typically do NOT need to add any of these scope strings into your SPA or API configuration; they are strictly for the internal custom policy execution pipeline. 3. Enable Custom Policy feature (Identity Experience Framework blade) if not already visible. 4. Upload the policies in this exact order (use the Upload button each time):
TrustFrameworkBase.xmlTrustFrameworkLocalization.xmlTrustFrameworkExtensions.xmlSignUpOrSignin.xml(PolicyIdB2C_1A_signup_signininside – RP referencing journeySignUpOrSignIn)- (Optional)
ProfileEdit.xml - (Optional)
PasswordReset.xml - After upload of the first four, run the sign-up/sign-in policy (Run now) to sanity check – a sign‑up form should appear.
- Any future changes: update locally, then re‑upload just the changed file(s). Base rarely changes; most edits land in
TrustFrameworkExtensions.xml.
Key customizations in the LocalAccounts policies:
- Added REST technical profile
REST-UserValidationexecuted during sign‑up before directory write (validates existence / blocked state via/users/validate). - (Legacy plan items like CRM contact creation & error simulation are not wired into the LocalAccounts journey – see
docs/b2c-custom-policy-plan.mdfor future integration notes.) - Structured error codes mapped in
TrustFrameworkLocalization.xml(idm.user.blocked,idm.throttle, etc.). - Custom error page override (
api.error) pointing to placeholder URL – update before production. - Localization file supplies user‑friendly error and field text.
| Location | Value |
|---|---|
backend/appsettings.json:AzureAdB2C:Audience |
The Application ID URI you set (e.g. https://<tenant>.onmicrosoft.com/api) |
backend/appsettings.json:AzureAdB2C:ClientId |
API app registration Client ID |
frontend environment.b2c.clientId |
SPA app registration Client ID |
frontend environment.b2c.apiScopes |
Full scope string (e.g. https://<tenant>.onmicrosoft.com/api/user.read) |
Swagger UI now includes a Bearer auth scheme. Acquire a token in the SPA (F12 → Application/Local Storage or network), then:
- Open
/swagger. - Click Authorize → enter:
Bearer <access_token>. - Try
GET /api/debug/tokento view claim set.
Update backend/appsettings.json (values already committed in this sample):
"AzureAdB2C": {
"Instance": "https://zipzappmetadatadev.b2clogin.com",
"Domain": "zipzappmetadatadev.onmicrosoft.com",
"TenantId": "zipzappmetadatadev.onmicrosoft.com",
"ClientId": "f6bbbb2d-02dd-4ca4-83bd-bf0709107617", // API application (protected resource) client ID
"SignUpSignInPolicyId": "B2C_1A_SignUpSignIn", // Custom policy Id (renamed to avoid built-in flow collision)
"Audience": "https://zipzappmetadatadev.onmicrosoft.com/contoso-transit-api" // Application ID URI of API
}
Update frontend/src/environments/environment.ts (values already present):
tenant: 'zipzappmetadatadev.onmicrosoft.com'
clientId: 'd35ebbe9-3406-4c0b-bad1-8d92c47341b1' // SPA app registration client ID
signInSignUpPolicy: 'B2C_1A_SignUpSignIn'
apiScopes: ['https://zipzappmetadatadev.onmicrosoft.com/contoso-transit-api/user.read']
authorityDomain: 'zipzappmetadatadev.b2clogin.com'
If you fork this repository, consider moving these identifiers to environment‑specific configuration or secrets management (never store certificates or secrets directly in source).
cd frontend
npm install
cd ../backend
dotnet restore
In two terminals:
cd backend
dotnet run
cd frontend
npm start
Visit http://localhost:4200 and sign in.
- MSAL Angular acquires tokens from authority:
https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1A_SignUpSignIn. - Interceptor attaches access token to calls matching
environment.api.baseUrl. - .NET API validates token via
Microsoft.AspNetCore.Authentication.JwtBeareragainst the B2C policy authority. - Audience & issuer enforced; on success returns filtered claims via
/api/profile.
| Goal | Change |
|---|---|
| Add another policy (e.g. password reset) | Add a new route initiating loginRedirect with different authority. |
| Acquire token for downstream API | Use acquireTokenSilent with additional scopes. |
| Add refresh semantics | MSAL handles per token expiry; intercept 401 to force silent/interactive reauth. |
| Add reactive auth state | Use MsalBroadcastService to subscribe to in-progress and handle events |
| Switch fully to standalone bootstrap | Replace AppModule with bootstrapApplication(AppComponent) |
| Add ESLint | ng add @angular-eslint/schematics |
- Do NOT trust client claims; always validate on backend.
- Restrict CORS origins in production.
- Consider caching JWKS keys beyond built-in caching if high volume.
- Log correlation IDs from B2C (add to accepted claims list if included).
- HTTPS and reverse proxy (e.g. Azure Front Door / App Gateway)
- App Insights telemetry (frontend + backend)
- Retry & circuit breaker for downstream APIs
- Centralized error handling middleware
- Configuration via environment / Azure App Config
| Plan Section | Implementation Hook |
|---|---|
| Error Handling Strategy | Interceptor + backend middleware (extend). |
| Correlation ID Propagation | Add custom claim / header when available. |
| Secure Profile Endpoint | /api/profile protected route. |
| Observability | Add ASP.NET logging + integrate App Insights SDK later. |
Generated scaffold – customize as needed.
- API App:
contoso-transit-api→ Expose an API → Application ID URI =https://zipzappmetadatadev.onmicrosoft.com/contoso-transit-api→ scopeuser.read. - SPA App:
contoso-transit-spa→ SPA redirecthttp://localhost:4200→ add delegated permissionuser.read→ grant admin consent. - Custom Policies: Upload base, extensions, localization, sign-up-sign-in (PolicyId
B2C_1A_SignUpSignIn). - Backend:
Audiencematches Application ID URI;SignUpSignInPolicyId=B2C_1A_SignUpSignIn. - Frontend:
apiScopescontains full scope string ending in/user.read. - Test flow: Sign up → CRM & IDM orchestration executes → token issued → call
/api/profile.
| Scenario | Action |
|---|---|
| Update REST endpoint URL | Edit REST-UserValidation in TrustFrameworkExtensions.xml and re-upload that file only. |
| Add new output claim to token | Add claim to base schema (if new), then include in RelyingParty OutputClaims of SignUpOrSignin.xml; re-upload RP file. |
| Localize additional strings | Extend TrustFrameworkLocalization.xml with new LocalizedString entries. |
| Troubleshoot token issues | Use https://jwt.ms and check aud, iss, and presence of custom claims. |
| Capture runtime errors | Ensure backend logs correlation ID; optionally add App Insights REST logger technical profile. |
| Integrate CRM contact creation | Add new REST technical profile + orchestration step (before AAD write) in TrustFrameworkExtensions.xml; re-upload that file and test. |
| Point validation to local API | Change ServiceUrl in REST-UserValidation to your local URL (e.g. https://localhost:5001/users/validate). |
The controller UserValidationController implements a demo endpoint consumed by the custom policy REST call.
POST /users/validate
Request body example:
{
"email": "alice.legacy@example.com",
"correlationId": "3f6d9a28-1b63-4f13-9ed0-0f6e9acd1111"
}Successful (existing user) response:
{
"userExists": true,
"userId": "idm-a1b2c3d4e5",
"userMessage": null,
"errorCode": null,
"journeyHasError": false
}New user (not found) response:
{
"userExists": false,
"userId": null,
"userMessage": "User not found – proceed to create account.",
"errorCode": null,
"journeyHasError": false
}Error (blocked) response:
{
"userExists": false,
"userId": null,
"userMessage": "The specified account is blocked.",
"errorCode": "idm.user.blocked",
"journeyHasError": true
}Logic summary:
- Emails containing
legacyorexists=> treated as existing. - Emails containing
blocked=> returns error withjourneyHasError=true. - All others => new user path.
Adjust this stub to integrate with a real IDM: replace the simulated rules with a repository / API client and map your real service response fields to the expected claim names in the JSON payload.
To quickly validate custom error UI behavior (including retry countdown) without manipulating real downstream systems, the backend includes a simulation endpoint and the policy adds an REST-IDM-ErrorSimulation technical profile.
POST /users/simulate-error
Request:
{ "scenario": "throttle", "correlationId": "<guid>" }Supported scenarios:
| Scenario | Behavior | Error Code | retryAfter | Notes |
|---|---|---|---|---|
throttle |
Returns journey error with wait period | idm.throttle |
15 | Shows countdown on custom error page |
generic |
Returns generic journey error | idm.generic |
– | Default if omitted |
success |
Returns non-error payload | – | – | journeyHasError=false |
Throttle example:
{
"userExists": false,
"userId": null,
"userMessage": "Too many attempts. Please retry later.",
"errorCode": "idm.throttle",
"journeyHasError": true,
"retryAfter": 15
}Technical profile snippet (example only – for current implementation see TrustFrameworkExtensions.xml REST-UserValidation rather than the older simulation profile):
<TechnicalProfile Id="REST-IDM-ErrorSimulation">
<DisplayName>IDM Error Simulation</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider,Web.TPEngine" />
<Metadata>
<Item Key="ServiceUrl">https://idm-api.example.com/users/simulate-error</Item>
<Item Key="AuthenticationType">ClientCertificate</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="ResolveJsonPathsInJsonTokens">true</Item>
</Metadata>
<CryptographicKeys>
<Key Id="ClientCertificate" StorageReferenceId="B2C_1A_IDMApiClientCertificate" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="correlationId" PartnerClaimType="correlationId" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="journeyErrorMessage" PartnerClaimType="userMessage" />
<OutputClaim ClaimTypeReferenceId="journeyErrorCode" PartnerClaimType="errorCode" />
<OutputClaim ClaimTypeReferenceId="journeyHasError" PartnerClaimType="journeyHasError" />
<OutputClaim ClaimTypeReferenceId="journeyRetryAfter" PartnerClaimType="retryAfter" />
</OutputClaims>
</TechnicalProfile>Temporary orchestration step example (insert before SendClaims and renumber following steps):
<OrchestrationStep Order="X" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="IDM-Error-Sim" TechnicalProfileReferenceId="REST-IDM-ErrorSimulation" />
</ClaimsExchanges>
</OrchestrationStep>If the simulation returns journeyHasError=true, the existing error handler (SelfAsserted-ErrorHandler) triggers and the custom error.html displays correlation ID, code, message, support link, and retry countdown.
Remove the step when finished testing to restore normal journey flow.
The custom policy technical profile currently specifies AuthenticationType="ClientCertificate". To fully secure the call you have two main options:
| Option | Policy Setting | Backend Changes | When to Use |
|---|---|---|---|
| Mutual TLS (Client Certificate) | AuthenticationType=ClientCertificate + <CryptographicKeys><Key Id="ClientCertificate" StorageReferenceId="B2C_1A_IDMApiClientCertificate"/></CryptographicKeys> |
Enable client cert capture (Kestrel) and validate thumbprint(s) | Production / strong auth needed |
| Basic Auth (simpler) | AuthenticationType=Basic + keys for BasicAuthenticationUsername & BasicAuthenticationPassword |
Add Basic auth middleware verifying header | Quick dev / lower security |
Current repo implements an optional client certificate validation gate (see RestApiAuth section in appsettings.json). To activate:
- Upload your client cert to B2C policy keys with name
B2C_1A_IDMApiClientCertificate. - Get the thumbprint (uppercase, no spaces) and place it in
RestApiAuth:AllowedThumbprints. - Set
RestApiAuth:RequireClientCertificatetotrue(environment variable override recommended instead of committing value). - Ensure the REST technical profile still has
AuthenticationType=ClientCertificate.
If you prefer Basic auth instead (simpler locally):
- Create two policy keys (Manual):
B2C_1A_IdmApiBasicUsername,B2C_1A_IdmApiBasicPassword. - Change the technical profile metadata:
<Item Key="AuthenticationType">Basic</Item>and corresponding<CryptographicKeys>to use those keys (remove the certificate key for that profile). - Add Basic header validation middleware (not included yet) or switch the existing certificate middleware logic accordingly.
Important: Azure AD B2C cannot add arbitrary custom headers to REST calls—use only supported auth types. For production, prefer client certificate or a dedicated intermediary service protected by network controls.
The endpoint now looks up users from Data/users.json. To modify behavior:
- Edit the JSON file and save—it's auto reloaded when timestamp changes.
- Add fields or extend
DirectoryUserif you need additional claim outputs (also update technical profile output mapping and controller response). - For large datasets, replace
JsonUserDirectorywith a DB or caching layer.
Custom policy configuration complete with tenant: zipzappmetadatadev.onmicrosoft.com.
For the simplest possible deployment of the Contoso.IdentityApi to an existing Azure App Service Web App we provide scripts/Deploy-BackendApi.ps1. It wraps a dotnet publish followed by:
az webapp deploy --resource-group <rg> --name <webAppName> --src-path <zip> --type zip --restart true
- Azure CLI installed (
az version) - Logged in:
az login(and if you have multiple subscriptions:az account set --subscription <subIdOrName>) - Existing Web App (Linux or Windows) created for .NET 8 (runtime stack isn't strictly required when using self-contained publish, but this sample uses framework-dependent publish).
| Parameter | Required | Description |
|---|---|---|
-WebAppName |
Yes | Web App name OR full host (e.g. contoso-transit-api-dev-...azurewebsites.net) |
-ResourceGroup |
Yes | Resource group containing the Web App |
-ProjectPath |
No | Defaults to backend/Contoso.IdentityApi/Contoso.IdentityApi.csproj |
-Configuration |
No | Build configuration (default Release) |
-Framework |
No | Force a specific TFM (e.g. net8.0) |
-SkipBuild |
No | Skip dotnet publish (use existing folder) |
-ZipOnly |
No | Produce the ZIP without deploying |
-Force |
No | Overwrite existing ZIP if path collision |
# From repo root (Windows PowerShell / pwsh)
./scripts/Deploy-BackendApi.ps1 -WebAppName contoso-transit-api-dev-eba5fcfughcfg0c0 -ResourceGroup <your-resource-group>If you copy the full host name, the script will automatically strip the domain, e.g.:
./scripts/Deploy-BackendApi.ps1 -WebAppName contoso-transit-api-dev-eba5fcfughcfg0c0.australiaeast-01.azurewebsites.net -ResourceGroup <your-resource-group>Verbose build & force overwrite of an existing zip:
./scripts/Deploy-BackendApi.ps1 -WebAppName contoso-transit-api-dev-eba5fcfughcfg0c0 -ResourceGroup <rg> -Verbose -ForceProduce the ZIP artifact only (no deployment):
./scripts/Deploy-BackendApi.ps1 -WebAppName contoso-transit-api-dev-eba5fcfughcfg0c0 -ResourceGroup <rg> -ZipOnly- Publish output:
backend/Contoso.IdentityApi/bin/DeployPublish/<Configuration>-<timestamp>/ - ZIP artifact:
artifacts/Contoso.IdentityApi-<timestamp>.zip
- This is intentionally minimal. For blue/green or slot swaps, extend with
az webapp deployment slotcommands. - Add health warm-up by invoking the site root after deploy if desired.
- For CI, integrate this script in a pipeline and provide
WebAppName/ResourceGroupas variables or use nativeazure/webapps-deployGitHub Action. - To speed up repeat builds, add a
-SkipBuildmode paired with caching published output in CI (dotnet publish incremental builds are already incremental if obj/bin are preserved).
Enable a read‑only mounted ZIP (atomic style deploy) instead of file extraction:
./scripts/Deploy-BackendApi.ps1 -WebAppName <app> -ResourceGroup <rg> -RunFromPackage [-LegacyConfigZip]Behavior when -RunFromPackage is used:
- Ensures
WEBSITE_RUN_FROM_PACKAGE=1(skips if already a URL value). - By default uses
az webapp deploy --type zip(current recommended command – the platform mounts the package automatically when the setting is present). - Sentinel DLL (
Contoso.IdentityApi.dll) check before upload. - Optional
-LegacyConfigZipforces the deprecatedwebapp deployment source config-zippath (only for troubleshooting older behavior; expect a deprecation warning). If mounting fails, inspect Kudu event logs or redeploy without-RunFromPackage.
The Angular SPA can be deployed to a standard Azure App Service (Linux or Windows) with the companion script scripts/Deploy-Frontend.ps1. This mirrors the backend deployment approach: build → zip → az webapp deploy.
- Azure CLI logged in (
az login) - Node.js / npm
- (Optional) If a runtime-specific App Service plan is used (e.g. Node), ensure it's compatible with serving static assets. Otherwise any App Service can serve static files from
wwwroot.
| Parameter | Required | Description |
|---|---|---|
-WebAppName |
Yes | Web App name or full host (script strips domain) |
-ResourceGroup |
Yes | Resource group containing the Web App |
-FrontendDir |
No | Path to frontend root (default frontend) |
-ProjectName |
No | Angular project (defaults from angular.json) |
-Configuration |
No | Angular build config (production default) |
-DistPath |
No | Override dist output path |
-ZipPath |
No | Override artifact zip path |
-SkipInstall |
No | Skip npm install |
-SkipBuild |
No | Skip Angular build |
-ZipOnly |
No | Produce ZIP without deploy |
-Force |
No | Overwrite existing ZIP |
./scripts/Deploy-Frontend.ps1 -WebAppName contoso-transit-frontend-dev -ResourceGroup <rg>If you copy the full host name:
./scripts/Deploy-Frontend.ps1 -WebAppName contoso-transit-frontend-dev.azurewebsites.net -ResourceGroup <rg>Skip reinstalling dependencies (useful in CI with caching) & force overwrite:
./scripts/Deploy-Frontend.ps1 -WebAppName contoso-transit-frontend-dev -ResourceGroup <rg> -SkipInstall -ForceArtifact only:
./scripts/Deploy-Frontend.ps1 -WebAppName contoso-transit-frontend-dev -ResourceGroup <rg> -ZipOnly- Build output (Angular):
frontend/dist/b2c-frontend/ - ZIP artifact:
artifacts/frontend-b2c-frontend-<timestamp>.zip
- For SPA routing (client-side routes) configure a fallback rewrite rule in App Service (via
web.configfor Windows/IIS or add a simple Node server). Alternatively, add aweb.configat the dist root mapping all unmatched paths toindex.html. - Consider using Azure Static Web Apps for global edge distribution and built‑in auth if requirements evolve.
- To add a fallback now, create
frontend/src/web.configbefore build with a rewrite rule and list it underassets.
./scripts/Deploy-Frontend.ps1 -WebAppName <app> -ResourceGroup <rg> -RunFromPackage [-LegacyConfigZip]When specified:
- Ensures
WEBSITE_RUN_FROM_PACKAGE=1. - Uses
az webapp deployby default (mounts when setting is present). index.htmlsentinel validation.-LegacyConfigZipavailable only if you need to compare legacy behavior (emits deprecation warning). For SPA routing add aweb.configrewrite so deep links resolve under run-from-package.