Files
apf_portal/apps/portal-bff/.env.example
T
Julien Gautier 77b2f73172
CI / scan (pull_request) Successful in 2m32s
CI / commits (pull_request) Successful in 2m46s
CI / check (pull_request) Successful in 4m22s
CI / a11y (pull_request) Successful in 1m52s
CI / perf (pull_request) Successful in 5m19s
feat(portal-bff): /auth/login route — pkce flow start + signed cookie
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.
2026-05-12 10:53:32 +02:00

110 lines
5.5 KiB
Bash

# BFF environment template
# Copy to .env (which is gitignored) and fill in actual values for local development.
# Production values are managed by the platform's secret manager (see future infrastructure ADR).
# Postgres connection (per ADR-0006)
# Local dev default: dockerised Postgres on port 5432, schema 'public'.
# Username / password / db must match infra/local/.env (POSTGRES_USER /
# POSTGRES_PASSWORD / POSTGRES_DB) — those are the source of truth,
# this is the BFF view of the same connection.
#
# IMPORTANT — URL encoding. The password is part of the URL userinfo
# segment, so any of these characters must be URL-encoded:
# @ → %40 # → %23 : → %3A / → %2F ? → %3F
# % → %25 & → %26 = → %3D + → %2B ; → %3B
# i.e. if your POSTGRES_PASSWORD is "p@ss#1", DATABASE_URL must read
# "postgresql://portal:p%40ss%231@localhost:5432/portal_dev?schema=public"
# The BFF aborts at boot with a clear error if it detects an unencoded
# special character (see apps/portal-bff/src/config/check-database-url.ts).
DATABASE_URL="postgresql://portal:portal_dev_change_me@localhost:5432/portal_dev?schema=public"
# Observability (per ADR-0012)
# All OTEL_* keys are honoured by the OpenTelemetry SDK directly — see
# apps/portal-bff/src/observability/tracing.ts for the bootstrap.
# Pino log level: 'info' in prod, 'debug' in dev (default if unset).
LOG_LEVEL=debug
OTEL_SERVICE_NAME=portal-bff
OTEL_SERVICE_VERSION=dev
# Default endpoint targets the Collector provisioned in
# infra/local/dev.compose.yml. The /v1/traces suffix is required by
# the HTTP/Protobuf transport.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# v1 samples 100 % at the app; tail sampling is delegated to the
# Collector (per ADR-0012). Override only for spike investigations.
OTEL_TRACES_SAMPLER=always_on
# Identity / Entra ID app registration (per ADR-0008 / ADR-0009)
# Values come from the project's Entra application registration in the
# Azure Admin Center → App registrations → APF Portal. The four
# *_INSTANCE_URL / *_TENANT_ID / *_CLIENT_ID / *_CLIENT_SECRET keys
# are mandatory; the BFF refuses to boot without them (see
# apps/portal-bff/src/config/check-entra-config.ts).
#
# ENTRA_INSTANCE_URL is the Microsoft login endpoint — usually
# https://login.microsoftonline.com/. The authority used by MSAL is
# `${ENTRA_INSTANCE_URL}${ENTRA_TENANT_ID}` for single-tenant flows,
# or `${ENTRA_INSTANCE_URL}organizations` / `common` for multi-tenant
# (per ADR-0008's dual-audience design). v1 uses the tenant-scoped
# authority; the multi-tenant switch lands when External ID activation
# is needed.
#
# ENTRA_CLIENT_SECRET is the high-value secret of this set. Never
# commit a real value. Production manages it via the deploy platform's
# secret manager (future infrastructure ADR).
ENTRA_INSTANCE_URL=https://login.microsoftonline.com/
ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000
ENTRA_CLIENT_ID=00000000-0000-0000-0000-000000000000
ENTRA_CLIENT_SECRET=replace_with_real_value
# Redirect URIs registered in Entra alongside the same client id. Both
# `/auth/callback` and `/auth/logout` paths are mounted by the BFF
# once the OIDC routes land in a subsequent PR.
ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/callback
ENTRA_POST_LOGOUT_REDIRECT_URI=http://localhost:4200/
# Cookie signing secret (per ADR-0009 §"Cookies"). Used to sign the
# transient pre-auth cookie that carries the OIDC `state` + PKCE
# verifier between the /auth/login redirect and the /auth/callback
# round-trip, and (once ADR-0010 ships) the session cookie's
# integrity layer. Mandatory at boot — the BFF aborts if missing or
# obviously weak (less than 32 base64-decoded bytes ≈ 256 bits of
# entropy). Generate a fresh value per environment:
#
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
SESSION_SECRET=replace_with_32_random_bytes_base64url
# Future env vars introduced by upcoming phases / ADRs:
#
# Auth flow (ADR-0009) — additional keys wired as the routes land:
# ENTRA_CLIENT_CERT_PATH (alternative to ENTRA_CLIENT_SECRET)
# ENTRA_ACCEPTED_TENANT_IDS (CSV; restricts which tenants can sign in
# in the multi-tenant phase — empty means
# "only ENTRA_TENANT_ID is accepted")
#
# Sessions (ADR-0010):
# REDIS_URL (or REDIS_SENTINEL_HOSTS + REDIS_SENTINEL_NAME)
# REDIS_PASSWORD
# REDIS_TLS ('true' in prod)
# SESSION_ENCRYPTION_KEY (32-byte base64)
# SESSION_IDLE_TIMEOUT_SECONDS (default 1800)
# SESSION_ABSOLUTE_TIMEOUT_SECONDS (default 43200)
#
# MFA (ADR-0011):
# MFA_FRESHNESS_SECONDS (default 600)
#
# Observability — additional keys to be wired as features land:
# LOG_USER_ID_SALT (per-environment salt for hashing user_id
# in CLS context — needed once auth lands)
#
# Audit trail (ADR-0013):
# AUDIT_DATABASE_URL (separate creds, role 'audit_writer')
# AUDIT_ARCHIVER_DATABASE_URL (role 'audit_archiver', for the retention purge job)
# AUDIT_RETENTION_DAYS (default 365)
#
# Downstream API access (ADR-0014):
# OBO_CACHE_ENCRYPTION_KEY (32-byte base64, distinct from SESSION_ENCRYPTION_KEY)
# BFF_JWKS_PRIVATE_KEY_PATH
# BFF_JWKS_KID
# <SERVICE>_API_BASE_URL (per integrated downstream)
# <SERVICE>_TIMEOUT_MS (optional, defaults to 5000)