From 951bdd8364ce3352a1bd197ad739c20214472791 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 16 Jul 2026 12:45:07 +0000 Subject: [PATCH] fix(infra): local compose parity + host-browser OIDC (closes #91) (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why Closes #91. `infra/docker-compose.local.yml` (the no-make local stack) was missing the `domain` service and all three portals, and never wired host-browser OIDC — so browsing the behandel portal redirected to `http://keycloak:8080/…`, which a host browser can't resolve. - **Parity**: add `domain`, `self-service`, `openbaar`, `behandel` (local now matches the CI-canonical `docker-compose.yml` service-for-service). - **BFF**: give it the Keycloak + downstream env it was missing (it previously fell back to appsettings and couldn't reach Keycloak). - **Host-browser OIDC**: pin Keycloak's frontend/issuer URL to `http://localhost:8180` (`KC_HOSTNAME`) with `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true`, so a host browser logs in on `localhost:8180` while the BFF still validates in-network via `keycloak:8080`. - **Portals**: bind-mount a `localhost:8180` `config.json` over the image's baked `keycloak:8080` one (`infra/local-config/*`). openbaar is anonymous, no config. ## How verified - `docker compose -f infra/docker-compose.local.yml config` valid; parity check shows nothing missing. - Started Keycloak from the local compose and confirmed the discovery document: - **host view** (`localhost:8180`): `issuer` + all endpoints on `localhost:8180` (what the browser uses). - **in-network view** (`keycloak:8080`): `issuer` stays `http://localhost:8180/...` (matches browser tokens) while `jwks_uri`/`token_endpoint` resolve to `keycloak:8080` (reachable by the BFF). ## Notes for reviewers - The full portal→BFF→Keycloak login round-trip should get a quick browser smoke test on a real engine (I validated the Keycloak issuer/backchannel split and compose validity, but can't drive a browser here). Ports: self-service :8140, openbaar :8141, behandel :8142; users in `docs/synthetic-data.md`. - On rootless podman the portal→BFF nginx proxy (`resolver 127.0.0.11`) may 502 (a separate known podman-vs-docker DNS quirk); login is a browser redirect and is unaffected. Works on Docker Desktop. - No app-code change; `docker-compose.yml` (CI-canonical) is untouched. Reviewed-on: https://git.labs.respellion.tech/eho/register-referentie/pulls/92 --- infra/docker-compose.local.yml | 121 +++++++++++++++++++- infra/local-config/behandel.config.json | 3 + infra/local-config/self-service.config.json | 3 + 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 infra/local-config/behandel.config.json create mode 100644 infra/local-config/self-service.config.json diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index 18d3f68..e380b8d 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -17,7 +17,12 @@ # # Port map (host): # 8000 OpenZaak · 8001 Open Notificaties · 8080 BFF · 8090 Flowable REST -# 8100 ACL · 8180 Keycloak (all admin: admin / admin — dev only) +# 8100 ACL · 8130 Domain · 8180 Keycloak (all admin: admin / admin — dev only) +# 8140 self-service portal · 8141 openbaar register · 8142 behandel portal +# +# Portal OIDC on the HOST: browse the portals at their 8140/8141/8142 ports and log in via +# Keycloak on localhost:8180 (KC_HOSTNAME below pins the issuer there; the BFF still validates +# in-network via keycloak:8080). Test users are in docs/synthetic-data.md. services: @@ -205,6 +210,12 @@ services: KEYCLOAK_ADMIN_PASSWORD: admin KC_HEALTH_ENABLED: "true" KC_HTTP_ENABLED: "true" + # Pin the frontend/issuer URL to the host-published address so a browser on the host and the + # tokens it gets both use localhost:8180. KC_HOSTNAME_BACKCHANNEL_DYNAMIC lets in-network + # callers (the BFF via keycloak:8080) still resolve token/jwks endpoints to their request host, + # so the BFF validates the localhost:8180 issuer while fetching keys over the compose network. + KC_HOSTNAME: http://localhost:8180 + KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" ports: - "8180:8080" volumes: @@ -295,6 +306,14 @@ services: context: ../services/bff dockerfile: Dockerfile image: register-referentie/bff:dev + environment: + # Reach Keycloak over the compose network for metadata/keys; the discovered issuer is the + # host-pinned localhost:8180 (KC_HOSTNAME above), which is what browser tokens carry — so + # validation matches without the BFF ever needing to resolve localhost:8180 itself. + Keycloak__Authority: http://keycloak:8080/realms/digid + Keycloak__MedewerkerAuthority: http://keycloak:8080/realms/medewerker + Downstream__Domain__BaseUrl: http://domain:8080/ + Downstream__Projection__BaseUrl: http://projection-api:8080/ ports: - "8080:8080" healthcheck: @@ -303,6 +322,39 @@ services: timeout: 3s retries: 5 start_period: 10s + depends_on: + domain: + condition: service_healthy + projection-api: + condition: service_healthy + keycloak: + condition: service_started + networks: [cg] + + # ── BIG Domain Service (S-05) ───────────────────────────────────────────── + domain: + build: + context: ../services/domain + dockerfile: Dockerfile + image: register-referentie/domain:dev + environment: + Flowable__BaseUrl: http://flowable-rest:8080/flowable-rest/ + Flowable__Username: rest-admin + Flowable__Password: test + Acl__BaseUrl: http://acl:8080/ + ports: + - "8130:8080" + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + depends_on: + acl: + condition: service_healthy + flowable-init: + condition: service_completed_successfully networks: [cg] # ── Read projection (S-06) ──────────────────────────────────────────────── @@ -362,6 +414,73 @@ services: condition: service_healthy networks: [cg] + # ── Portals (S-08/S-09/S-12) ────────────────────────────────────────────── + # nginx serves each Angular app and reverse-proxies its endpoint group to the BFF (same-origin). + # The images bake config.json with the compose authority (keycloak:8080), which a HOST browser + # can't resolve — so here we bind-mount a config.json pointing at the host-published localhost:8180 + # (matching KC_HOSTNAME). openbaar is anonymous and needs no config. + self-service: + build: + context: .. + dockerfile: apps/self-service/Dockerfile + image: register-referentie/self-service:dev + ports: + - "8140:80" + volumes: + - ./local-config/self-service.config.json:/usr/share/nginx/html/config.json:ro,z + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + depends_on: + bff: + condition: service_healthy + keycloak: + condition: service_started + networks: [cg] + + openbaar: + build: + context: .. + dockerfile: apps/openbaar/Dockerfile + image: register-referentie/openbaar:dev + ports: + - "8141:80" + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + depends_on: + bff: + condition: service_healthy + networks: [cg] + + behandel: + build: + context: .. + dockerfile: apps/behandel/Dockerfile + image: register-referentie/behandel:dev + ports: + - "8142:80" + volumes: + - ./local-config/behandel.config.json:/usr/share/nginx/html/config.json:ro,z + healthcheck: + test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + depends_on: + bff: + condition: service_healthy + keycloak: + condition: service_started + networks: [cg] + volumes: oz-db: nrc-db: diff --git a/infra/local-config/behandel.config.json b/infra/local-config/behandel.config.json new file mode 100644 index 0000000..71c0f65 --- /dev/null +++ b/infra/local-config/behandel.config.json @@ -0,0 +1,3 @@ +{ + "authority": "http://localhost:8180/realms/medewerker" +} diff --git a/infra/local-config/self-service.config.json b/infra/local-config/self-service.config.json new file mode 100644 index 0000000..7642cac --- /dev/null +++ b/infra/local-config/self-service.config.json @@ -0,0 +1,3 @@ +{ + "authority": "http://localhost:8180/realms/digid" +}