Security¶
The portfolio uses managed identities and Azure RBAC throughout — no long-lived credentials exist in code or containers.
Managed Identities¶
Each Container App has a user-assigned managed identity that is deployed unconditionally (even when deploy_containers=false) so role assignments propagate before the Container App is created.
| Identity | Resource | Role assignments |
|---|---|---|
dna-{env}-portfolio-we-id-fe |
Frontend Container App | None (static file serving only) |
dna-{env}-portfolio-we-id-be |
Backend Container App | AcrPull on ACR, Key Vault Secrets User on KV |
Managed identities use principalType: 'ServicePrincipal' in role assignment Bicep (not 'ManagedIdentity'). The principalId is read from the identity resource (identity_resource.properties.principalId), not from the Container App resource.
ACR Admin User¶
The Container Registry has adminUserEnabled: false. The backend managed identity is granted AcrPull — no shared admin credentials are used anywhere.
Key Vault¶
Key Vault is deployed with RBAC authorization mode (enableRbacAuthorization: true) — access policies are not used.
Soft delete and purge protection:
Secrets cannot be permanently deleted until the 7-day retention window expires, even by administrators.
Granting yourself Key Vault access:
Email Credentials Flow¶
EMAIL_ADDRESS and EMAIL_PASSWORD never appear as plaintext in code or logs:
- GitHub Secrets → Bicep
@secure()parameters → stored asemail-address/email-passwordKey Vault secrets on every infrastructure deploy - Backend Container App pulls KV secrets at startup via
keyVaultUrl+ managed identity (secretRef) - Backend reads
os.getenv("EMAIL_ADDRESS")/os.getenv("EMAIL_PASSWORD")— falls back to demo mode (logs only) if placeholders are present
RBAC Summary¶
| Principal | Scope | Role |
|---|---|---|
| Backend managed identity | ACR | AcrPull |
| Backend managed identity | Key Vault | Key Vault Secrets User |
| Backend managed identity | App Configuration store | App Configuration Data Owner |
| GitHub Actions OIDC (service principal) | Subscription | Contributor |
| GitHub Actions OIDC (service principal) | Subscription | Cost Management Reader |
Admin Authentication¶
The portfolio admin API uses Entra ID (Microsoft Azure AD) for JWT-based authentication. Write endpoints (create/update/delete for projects, experience, skills) require a valid Entra access token from a member of the portfolio-admins security group.
JWT Validation Chain¶
Admin write endpoints use the require_admin_group dependency (api/auth.py). It tries each step in order:
| Step | Condition | Behaviour |
|---|---|---|
| 1. Dev bypass | DISABLE_AUTH=true AND not prod URL |
Returns mock principal — local dev only |
| 2. Legacy token | LEGACY_ADMIN_TOKEN env var set |
Accepts static bearer token — migration window |
| 3. fastapi-azure-auth | BACKEND_APP_CLIENT_ID + AZURE_TENANT_ID set |
Validates auth-code-flow tokens issued by the app |
| 4. PyJWT + JWKS | Fallback when step 3 fails | Validates standalone access tokens (e.g. from UIGen OAuth) |
After token validation, the dependency checks ENTRA_ADMIN_GROUP_ID membership. If the groups claim is absent (Entra omits it when the user is in >200 groups), it falls back to a Microsoft Graph checkMemberGroups call.
UIGen Session Validation Endpoint¶
UIGen calls GET /auth/me after completing its OAuth code exchange to validate the session. This endpoint:
- Validates the Bearer token using
validate_entra_token(PyJWT + JWKS directly — not fastapi-azure-auth, which only accepts tokens from its own session flow) - Returns
{"id": "<oid>", "email": "...", "name": "...", "role": "admin|guest"} - Is registered at both
/api/auth/meand/auth/meinmain.py— the UIGen shim proxy uses the unprefixed path
Entra App Registration Requirements¶
The Entra app registration must be configured correctly for tokens to validate:
requestedAccessTokenVersion: 2— required to emit v2.0 tokens. PyJWT validates the issuer ashttps://login.microsoftonline.com/<tenant>/v2.0; v1.0 tokens have a different issuer format that fails validation.- SPA platform redirect URI — the localhost redirect URI must be registered on the SPA platform (not Web) for PKCE to work. The Web platform does not support PKCE's implicit grant avoidance, causing the authorization code exchange to fail in local dev.
api://<client-id>/adminscope — theadminscope is declared in the app manifest and used bySingleTenantAzureAuthorizationCodeBearer.
UIGen EasyAuth Exclusions (Dev)¶
On dev, the UIGen Container App uses EasyAuth (unauthenticatedClientAction: RedirectToLoginPage). The /auth/token and /auth/callback paths are excluded from the EasyAuth gate in container_apps.bicep. Without these exclusions, EasyAuth intercepts the OAuth token exchange and the login loop never completes.
Environment Variables¶
| Variable | Purpose | Default |
|---|---|---|
BACKEND_APP_CLIENT_ID |
Entra app client ID (JWT audience) | "" — auth disabled |
AZURE_TENANT_ID |
Entra tenant ID for JWKS URI + Graph | "" |
ENTRA_ADMIN_GROUP_ID |
Object ID of portfolio-admins group |
"" — any valid JWT accepted |
LEGACY_ADMIN_TOKEN |
Static bearer token (migration window) | "" — disabled |
DISABLE_AUTH |
Bypass JWT validation in non-prod | "false" |
Troubleshooting¶
Container App can't pull image from ACR¶
The backend managed identity needs AcrPull on the ACR. Check role assignments:
Backend can't read Key Vault secrets¶
Ensure the backend managed identity has Key Vault Secrets User on the vault:
az keyvault secret returns "Forbidden"¶
The vault uses RBAC (enableRbacAuthorization: true) — set-policy commands have no effect. Grant yourself Key Vault Secrets Officer at vault scope instead.
Admin API returns 401 / "Invalid or missing authentication token"¶
Token may be v1.0 format. Check the Entra app registration has requestedAccessTokenVersion: 2 in the manifest. v1.0 tokens have a different issuer (https://sts.windows.net/<tenant>/) that PyJWT rejects.
UIGen login loop never resolves on dev¶
EasyAuth is intercepting /auth/token or /auth/callback. Verify container_apps.bicep includes both paths in the UIGen EasyAuth exclusion list and redeploy infrastructure.
Next Steps¶
- Bicep Structure — how managed identities are defined in
container_apps.bicep - OIDC & Secrets — GitHub Actions OIDC setup and GitHub Secrets
- Resource Overview — naming and resource map
Page history
| Field | Value |
|---|---|
| Last updated | 2026-05-19 |
Changelog
| Date | PRs | Summary |
|---|---|---|
| 2026-03-10 | #66 | Extracted from infrastructure.md; managed identities, RBAC, Key Vault, email credentials flow |
| 2026-05-19 | #384, #438, #439, #449, #451, #452 | Add Entra ID admin auth section: JWT validation chain, UIGen session endpoint, PKCE requirements, EasyAuth exclusions, environment variables |