Files
apf_portal/package.json
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

160 lines
5.1 KiB
JSON

{
"name": "apf-portal",
"version": "0.0.0",
"license": "MIT",
"packageManager": "pnpm@10.33.4",
"scripts": {
"prepare": "husky",
"ci:check": "pnpm exec nx affected -t format:check lint test build",
"ci:audit": "pnpm audit --audit-level=moderate",
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm exec lhci autorun --config=./lighthouserc.js"
},
"private": true,
"lint-staged": {
"!(pnpm-lock).{ts,tsx,js,jsx,html,scss,css,md,json,yml,yaml}": [
"prettier --write"
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@nestjs/core",
"@parcel/watcher",
"@prisma/client",
"@prisma/engines",
"@swc/core",
"esbuild",
"less",
"lmdb",
"msgpackr-extract",
"nx",
"prisma",
"unrs-resolver"
],
"overrides": {
"axios@<1.15.2": ">=1.15.2",
"@angular-devkit/core>ajv@<8.18.0": ">=8.18.0",
"brace-expansion@<5.0.5": ">=5.0.5",
"follow-redirects@<1.16.0": ">=1.16.0",
"ip-address@<10.1.1": ">=10.1.1",
"tmp@<0.2.4": ">=0.2.4",
"yaml@<2.8.3": ">=2.8.3"
}
},
"prisma": {
"schema": "apps/portal-bff/prisma/schema.prisma"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "~2.5.0",
"@analogjs/vitest-angular": "~2.5.0",
"@angular-devkit/core": "~21.2.0",
"@angular-devkit/schematics": "~21.2.0",
"@angular/build": "~21.2.0",
"@angular/cli": "~21.2.0",
"@angular/compiler-cli": "~21.2.0",
"@angular/language-service": "~21.2.0",
"@commitlint/cli": "^20.5.3",
"@commitlint/config-conventional": "^20.5.3",
"@eslint/js": "^9.8.0",
"@lhci/cli": "^0.15.1",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@nx/angular": "^22.7.1",
"@nx/devkit": "22.7.1",
"@nx/eslint": "^22.7.1",
"@nx/eslint-plugin": "22.7.1",
"@nx/jest": "22.7.1",
"@nx/js": "22.7.1",
"@nx/nest": "^22.7.1",
"@nx/node": "22.7.1",
"@nx/playwright": "22.7.1",
"@nx/vite": "^22.7.1",
"@nx/vitest": "22.7.1",
"@nx/web": "22.7.1",
"@nx/webpack": "22.7.1",
"@oxc-project/runtime": "^0.129.0",
"@playwright/test": "^1.36.0",
"@schematics/angular": "~21.2.0",
"@swc-node/register": "1.11.1",
"@swc/core": "1.15.33",
"@swc/helpers": "0.5.21",
"@tailwindcss/postcss": "^4.2.4",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "24.12.4",
"@typescript-eslint/utils": "^8.40.0",
"@vitest/coverage-v8": "~4.1.0",
"@vitest/ui": "~4.1.0",
"angular-eslint": "^21.2.0",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-playwright": "^1.6.2",
"husky": "^9.1.7",
"jest": "^30.0.2",
"jest-environment-node": "^30.0.2",
"jest-util": "^30.0.2",
"jsdom": "^29.0.0",
"jsonc-eslint-parser": "^2.1.0",
"lint-staged": "^17.0.0",
"nx": "22.7.1",
"pino-pretty": "^13.1.3",
"postcss": "^8.5.12",
"prettier": "^3.8.1",
"prisma": "^6.19.3",
"tailwindcss": "^4.2.4",
"ts-jest": "^29.4.0",
"ts-node": "10.9.2",
"tslib": "^2.3.0",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^8.0.0",
"vitest": "^4.0.8",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@angular/cdk": "~21.2.10",
"@angular/common": "~21.2.0",
"@angular/compiler": "~21.2.0",
"@angular/core": "~21.2.0",
"@angular/forms": "~21.2.0",
"@angular/localize": "~21.2.12",
"@angular/platform-browser": "~21.2.0",
"@angular/router": "~21.2.0",
"@azure/msal-node": "^5.2.1",
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.217.0",
"@opentelemetry/instrumentation": "^0.217.0",
"@opentelemetry/instrumentation-document-load": "^0.62.0",
"@opentelemetry/instrumentation-express": "^0.65.0",
"@opentelemetry/instrumentation-fetch": "^0.217.0",
"@opentelemetry/instrumentation-http": "^0.217.0",
"@opentelemetry/instrumentation-ioredis": "^0.65.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.63.0",
"@opentelemetry/instrumentation-pg": "^0.69.0",
"@opentelemetry/instrumentation-pino": "^0.63.0",
"@opentelemetry/instrumentation-user-interaction": "^0.61.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-node": "^0.217.0",
"@opentelemetry/sdk-trace-web": "^2.7.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@prisma/client": "^6.19.3",
"axios": "^1.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"cookie-parser": "^1.4.7",
"lucide-angular": "^1.0.0",
"nestjs-cls": "^6.2.0",
"nestjs-pino": "^4.6.1",
"nestjs-prisma": "^0.27.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"reflect-metadata": "^0.2.0",
"rxjs": "~7.8.0"
}
}