feat(portal-bff): entra config foundation — boot validator + auth module #102
Reference in New Issue
Block a user
Delete Branch "feat/portal-bff/auth-entra-config"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
First step of ADR-0009 wiring on the BFF: capture the Entra app-registration env vars in the boot pipeline so subsequent PRs can plug
@azure/msal-nodeonto a typed, already-validated config without re-readingprocess.env. No MSAL client, no OIDC routes, no session integration yet — those land in follow-up PRs.What lands
.env.examplepromotes the Entra block from its previous "future-vars" comment stub to an active section. Six keys:ENTRA_INSTANCE_URL— the Microsoft login endpoint (e.g.https://login.microsoftonline.com/).ENTRA_TENANT_ID,ENTRA_CLIENT_ID,ENTRA_CLIENT_SECRET— the values from the Entra app-registration UI.ENTRA_REDIRECT_URI,ENTRA_POST_LOGOUT_REDIRECT_URI— consumed by the OIDC routes in a follow-up PR.Multi-tenant
ENTRA_ACCEPTED_TENANT_IDSstays in the future-vars comment until External ID activation (ADR-0008 phase 2).apps/portal-bff/src/config/check-entra-config.ts— boot-time validator mirroringcheck-database-url.ts. Verifies every required key is present, the instance URL ishttps://and ends with/, tenant + client IDs are UUIDs, none of them are the literal placeholder values from.env.example, and the two redirect URIs parse as URLs. Returns a typedEntraConfigobject with a pre-computedauthorityfield (${instanceUrl}${tenantId}) so the future MSAL factory does not re-derive it.auth.module.ts—AuthModulewhose v1 surface is one provider: the parsedEntraConfigkeyed by theENTRA_CONFIGinjection token. Factory delegates toassertEntraConfig(). Non-global on purpose — consumers state intent by importing the module.Bootstrap wiring —
main.tscallsassertEntraConfig()alongsideassertDatabaseUrl()so misconfiguration fails fast at boot rather than mid-request (per ADR-0018 §"BFF env-var loading").AppModuleimportsAuthModule.Naming choice
Chose
ENTRA_*rather thanAZURE_AD_*to align with the ADR text (Microsoft Entra ID, post-2023 rebrand). The values you copy from the Entra app-registration UI go intoapps/portal-bff/.env(git-ignored).Decisions worth flagging
main.ts(boot-time fail-fast) and once in theAuthModulefactory (to obtain the value for DI). Both reads are idempotent and trivially cheap. The duplication is intentional: boot-time gives a clear, pre-NestFactory error; the factory call surfaces the typed value to consumers.@azure/msal-nodedependency added yet — introducing the dep without a consumer would be a smell. Lands in the next PR alongside the MSAL client factory.authorityin the parsed config rather than letting each MSAL consumer concatenateinstanceUrl + tenantId. One place to change if the multi-tenant authority (/organizations,/common) replaces the tenant-scoped one when External ID activates.Verification
nx run-many -t lint test build --projects=portal-bff— green..env.example,nx serve portal-bffaborts immediately withENTRA_CLIENT_ID is still the .env.example placeholder (…). With real values in a local.env, the BFF starts normally.Test plan
apps/portal-bff/.env,nx serve portal-bffboots clean.ENTRA_*keys → BFF aborts at boot with a clear message naming the missing key.Next PRs on the auth track
@azure/msal-node, add theMsalConfidentialClientfactory provider inAuthModule, expose it via DI./api/auth/login(PKCE-initiated redirect to Entra) +/api/auth/callback(token exchange + ID-token validation, audit-logged, no session persistence yet).__Host-portal_sessioncookie). Closes the auth loop.First step of ADR-0009 wiring. Captures the Entra app-registration env vars in the boot pipeline so subsequent PRs can plug `@azure/msal-node` straight onto a typed, already-validated config without re-reading process.env. No MSAL client, no OIDC routes, no session integration yet — those land in follow-up PRs. What lands: - `.env.example` promotes the Entra block from its previous "Future env vars" comment stub to an active configuration section. Six keys: `ENTRA_INSTANCE_URL`, `ENTRA_TENANT_ID`, `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET`, `ENTRA_REDIRECT_URI`, `ENTRA_POST_LOGOUT_REDIRECT_URI`. UUID + URL placeholders so the spec test for the "still-the-placeholder" guard has a real target. Multi-tenant `ENTRA_ACCEPTED_TENANT_IDS` stays in the future-vars comment until External ID activation lands. - `apps/portal-bff/src/config/check-entra-config.ts` — boot validator mirroring `check-database-url.ts`. Asserts every required key is present, validates the instance URL is `https://` and ends with `/`, the tenant + client IDs are UUIDs, none of them are the literal .env.example placeholder, and the two redirect URIs are parseable URLs. Returns a typed `EntraConfig` object with a pre-computed `authority` field (`${instanceUrl}${tenantId}`) so the MSAL factory in the next PR does not re-derive it. - `check-entra-config.spec.ts` — covers the happy path plus eight failure modes (missing keys, non-https instance, missing trailing slash, non-UUID tenant, placeholder UUID, placeholder secret, invalid redirect URI, http redirect for local dev). - `apps/portal-bff/src/auth/auth.module.ts` — `AuthModule` whose v1 surface is a single provider: the parsed `EntraConfig` keyed by the `ENTRA_CONFIG` injection token. Factory delegates to `assertEntraConfig()`. Module is non-global so consumers state intent by importing it. - `apps/portal-bff/src/auth/entra-config.token.ts` — `ENTRA_CONFIG` string token + `EntraConfig` type re-export. The pattern subsequent modules will use: `@Inject(ENTRA_CONFIG) private readonly entra: EntraConfig`. - `auth.module.spec.ts` — verifies the provider resolves the typed config, and that compilation fails when an env var is missing (boot-failure behaviour is preserved across the DI boundary). - `main.ts` calls `assertEntraConfig()` alongside `assertDatabaseUrl()` so misconfiguration fails fast at boot rather than mid-request (per ADR-0018 §"BFF env-var loading"). - `AppModule` imports `AuthModule`. Verification: 29 / 29 specs (was 20; +9 from the new entra-config spec + auth.module spec), lint clean, webpack build green. Naming: chose `ENTRA_*` rather than `AZURE_AD_*` to align with ADR text (Microsoft Entra ID, post-2023 rebrand). The values you copy from the Entra app-registration UI go into `apps/portal-bff/.env` (git-ignored).