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.
This commit is contained in:
@@ -79,6 +79,8 @@
|
||||
"@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",
|
||||
@@ -144,6 +146,7 @@
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user