feat: switch oidc client to confidential (backend token exchange)

The provisioned IdP client (https://auth.forgecore.work) is confidential
rather than public/PKCE-only, so a client secret must never reach the
browser. The frontend now only performs the Authorization Code + PKCE
redirect itself (hand-rolled PKCE, oidc-client-ts dependency removed)
and hands the resulting code + verifier to a new, intentionally
unauthenticated POST /api/v1/auth/session endpoint, which performs the
code-for-tokens exchange server-side using OIDC_CLIENT_SECRET and
returns only {accessToken, expiresIn} — refresh_token/id_token are
never forwarded to the client.

New required backend env vars: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET.
Added frontend/proxy.conf.json so the Angular dev server forwards
/api and /health to the local API without needing CORS.
This commit is contained in:
Bastian Wagner
2026-08-17 16:36:49 +02:00
parent 8eb5f0a3ed
commit 981cecbcbd
26 changed files with 554 additions and 58 deletions

View File

@@ -1325,3 +1325,18 @@ Do not start any of the following during this phase; each belongs to a later, ex
- Payment/booking automation (permanent non-goal).
Phase 03 begins only after the Phase 02 acceptance checklist is reviewed and green.
---
## Addendum: Confidential IdP Client Requires Backend Token Exchange (2026-08-17)
The plan's Task 10 assumed a public, PKCE-only SPA client and used `oidc-client-ts` to perform the full Authorization Code + PKCE token exchange directly in the browser. During manual end-to-end testing against the actual target IdP (`https://auth.forgecore.work`), it turned out the provisioned client is **confidential** (it has a client secret) rather than public. A client secret must never be shipped in a browser bundle, so this required a real, user-confirmed architecture change rather than a workaround:
- The frontend still performs the Authorization Code + PKCE redirect itself (hand-rolled PKCE generation in `frontend/src/app/auth/pkce.ts`; `oidc-client-ts` was removed as a dependency since its callback handling assumes a direct-to-IdP token exchange that doesn't fit this model).
- The resulting authorization `code` and PKCE `code_verifier` are POSTed to a new, intentionally unauthenticated backend endpoint, `POST /api/v1/auth/session` (`AuthSessionController``TokenExchangeService`), which performs the code-for-tokens exchange using `OIDC_CLIENT_SECRET` server-side and returns only `{ accessToken, expiresIn }``refresh_token`/`id_token` are deliberately never forwarded to the frontend.
- New required backend env vars: `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` (in addition to the already-planned `OIDC_ISSUER`/`OIDC_AUDIENCE`).
- `OIDC_AUDIENCE` is operationally set equal to `OIDC_CLIENT_ID` for this IdP, since it does not issue a separate API-resource audience; no code change was needed for this, only a configuration convention.
- `OidcDiscoveryService` now also captures `token_endpoint` (in addition to `jwks_uri`), needed for the backend-side token exchange.
- A local dev proxy (`frontend/proxy.conf.json`, wired into `angular.json`'s `serve` target) forwards `/api/*` and `/health/*` from the Angular dev server to the API, avoiding a need for CORS configuration in local development (production already avoids CORS entirely since `edge` serves both origins).
This is a deployment-specific IdP constraint, not a general Travel Planner requirement — a future public-client IdP could skip the backend proxy — but the BFF pattern is what ships today since it is what the actual configured IdP requires.