Third step of ADR-0009 wiring. Adds the first OIDC route:
`GET /api/auth/login` 302s to Entra's authorize endpoint with a
freshly-generated state + PKCE challenge, and stashes the matching
`{state, codeVerifier}` payload in a short-lived signed cookie so
the next-PR callback can verify the round-trip.
What lands:
- `cookie-parser` + `@types/express` added as deps. main.ts mounts
the cookie parser middleware with the `SESSION_SECRET` signing
key — signed cookies become available via `req.signedCookies` for
the upcoming callback.
- `.env.example` promotes `SESSION_SECRET` from a future-vars
comment into an active variable, with a one-liner showing how to
generate a 32-byte base64url value.
- `apps/portal-bff/src/config/check-session-secret.ts` — same family
of boot-time guard as `check-database-url.ts` /
`check-entra-config.ts`: refuses to start if `SESSION_SECRET` is
unset, still the .env.example placeholder, or decodes below 32
bytes of entropy.
- `apps/portal-bff/src/auth/auth.service.ts` —
`AuthService.beginAuthCodeFlow()` uses MSAL Node's `CryptoProvider`
for canonical PKCE verifier / challenge generation and state
nonce (fresh GUID per call), calls `msal.getAuthCodeUrl()` with
the configured redirect URI + OIDC scopes
(`openid profile email` — no `offline_access`; sessions are
short-lived and re-auth via Entra rather than refresh-token
shenanigans, per ADR-0010), and returns the URL + pre-auth
payload to the controller.
- `apps/portal-bff/src/auth/auth.cookie.ts` — pre-auth cookie name
(`portal_pre_auth`), 5-minute TTL, and shared `CookieOptions`:
`signed: true`, `httpOnly: true`, `sameSite: 'lax'` (allows
Entra's cross-site top-level redirect back), `secure` toggled by
`NODE_ENV`. The `__Host-` prefix waits for the prod TLS hardening
ADR.
- `apps/portal-bff/src/auth/auth.controller.ts` —
`@Controller('auth') GET login`: writes the cookie via Express
`res.cookie()` (using `@Res()`) then 302s to the auth URL. Thin
shell around `AuthService`.
- `AuthModule` registers the controller + service alongside the
existing `ENTRA_CONFIG` and `MSAL_CLIENT` providers.
Verification:
- `nx run-many -t lint test build --projects=portal-bff` — green.
- 39/39 specs (was 30; +9 across the new validator spec, service
spec, and controller spec).
- The service spec mocks MSAL's `getAuthCodeUrl` and asserts the
redirect URI / scopes / S256 method, the state-verifier identity
between the cookie payload and what's sent to Entra, and the
fresh-per-call replay protection.
- The controller spec asserts the cookie name + options + payload
serialization and the 302 redirect.
What this PR explicitly does NOT do:
- The callback. `GET /auth/callback` reads the cookie, exchanges
the code for tokens via `acquireTokenByCode`, validates the ID
token, and (next PR after) creates a session. If you click
`/auth/login` today, you'll round-trip through Entra and land on
a 404 — by design until the callback ships.
- Session persistence (waits on ADR-0010 Redis wiring).
- Logout, CSRF protection, route guards.