feat(portal-bff): entra config foundation — boot validator + auth module #102

Merged
julien merged 1 commits from feat/portal-bff/auth-entra-config into main 2026-05-12 02:27:56 +02:00
Owner

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-node 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-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_IDS stays 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 mirroring check-database-url.ts. Verifies every required key is present, the instance URL is https:// 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 typed EntraConfig object with a pre-computed authority field (${instanceUrl}${tenantId}) so the future MSAL factory does not re-derive it.

  • auth.module.tsAuthModule whose v1 surface is one provider: the parsed EntraConfig keyed by the ENTRA_CONFIG injection token. Factory delegates to assertEntraConfig(). Non-global on purpose — consumers state intent by importing the module.

  • Bootstrap wiringmain.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.

Naming choice

Chose ENTRA_* rather than AZURE_AD_* to align with the 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).

Decisions worth flagging

  • Validator called twice — once in main.ts (boot-time fail-fast) and once in the AuthModule factory (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.
  • No @azure/msal-node dependency added yet — introducing the dep without a consumer would be a smell. Lands in the next PR alongside the MSAL client factory.
  • Pre-computed authority in the parsed config rather than letting each MSAL consumer concatenate instanceUrl + 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.
  • 29 / 29 specs (was 20; +9 from the new entra-config spec + auth.module spec).
  • Boot smoke test (manual): with the placeholder values in .env.example, nx serve portal-bff aborts immediately with ENTRA_CLIENT_ID is still the .env.example placeholder (…). With real values in a local .env, the BFF starts normally.

Test plan

  • Lint + test + build green.
  • Validator unit-test covers happy path + every documented failure mode.
  • Manual: drop the real Entra values you obtained into apps/portal-bff/.env, nx serve portal-bff boots clean.
  • Manual: temporarily blank out one of the four ENTRA_* keys → BFF aborts at boot with a clear message naming the missing key.

Next PRs on the auth track

  1. Install @azure/msal-node, add the MsalConfidentialClient factory provider in AuthModule, expose it via DI.
  2. First OIDC routes: /api/auth/login (PKCE-initiated redirect to Entra) + /api/auth/callback (token exchange + ID-token validation, audit-logged, no session persistence yet).
  3. Session persistence per ADR-0010 (Redis + AES-GCM, __Host-portal_session cookie). Closes the auth loop.
  4. RP-initiated logout, CSRF protection, route guards.
## 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-node` 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`](apps/portal-bff/.env.example)** promotes 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_IDS` stays in the future-vars comment until External ID activation (ADR-0008 phase 2). - **[`apps/portal-bff/src/config/check-entra-config.ts`](apps/portal-bff/src/config/check-entra-config.ts)** — boot-time validator mirroring `check-database-url.ts`. Verifies every required key is present, the instance URL is `https://` 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 typed `EntraConfig` object with a pre-computed `authority` field (`${instanceUrl}${tenantId}`) so the future MSAL factory does not re-derive it. - **[`auth.module.ts`](apps/portal-bff/src/auth/auth.module.ts)** — `AuthModule` whose v1 surface is one provider: the parsed `EntraConfig` keyed by the `ENTRA_CONFIG` injection token. Factory delegates to `assertEntraConfig()`. Non-global on purpose — consumers state intent by importing the module. - **Bootstrap wiring** — `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`. ## Naming choice Chose `ENTRA_*` rather than `AZURE_AD_*` to align with the 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). ## Decisions worth flagging - **Validator called twice** — once in `main.ts` (boot-time fail-fast) and once in the `AuthModule` factory (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. - **No `@azure/msal-node` dependency added yet** — introducing the dep without a consumer would be a smell. Lands in the next PR alongside the MSAL client factory. - **Pre-computed `authority`** in the parsed config rather than letting each MSAL consumer concatenate `instanceUrl + 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. - **29 / 29 specs** (was 20; +9 from the new entra-config spec + auth.module spec). - Boot smoke test (manual): with the placeholder values in `.env.example`, `nx serve portal-bff` aborts immediately with `ENTRA_CLIENT_ID is still the .env.example placeholder (…)`. With real values in a local `.env`, the BFF starts normally. ## Test plan - [x] Lint + test + build green. - [x] Validator unit-test covers happy path + every documented failure mode. - [ ] Manual: drop the real Entra values you obtained into `apps/portal-bff/.env`, `nx serve portal-bff` boots clean. - [ ] Manual: temporarily blank out one of the four `ENTRA_*` keys → BFF aborts at boot with a clear message naming the missing key. ## Next PRs on the auth track 1. Install `@azure/msal-node`, add the `MsalConfidentialClient` factory provider in `AuthModule`, expose it via DI. 2. First OIDC routes: `/api/auth/login` (PKCE-initiated redirect to Entra) + `/api/auth/callback` (token exchange + ID-token validation, audit-logged, no session persistence yet). 3. Session persistence per ADR-0010 (Redis + AES-GCM, `__Host-portal_session` cookie). Closes the auth loop. 4. RP-initiated logout, CSRF protection, route guards.
julien added 1 commit 2026-05-12 02:25:44 +02:00
feat(portal-bff): entra config foundation — boot validator + auth module
CI / scan (pull_request) Successful in 2m21s
CI / commits (pull_request) Successful in 2m31s
CI / check (pull_request) Successful in 2m37s
CI / a11y (pull_request) Successful in 2m7s
CI / perf (pull_request) Successful in 4m54s
7992a5ba14
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).
julien merged commit 58e3b65bd9 into main 2026-05-12 02:27:56 +02:00
julien deleted branch feat/portal-bff/auth-entra-config 2026-05-12 02:28:00 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#102