From fea806848b567b392812ba4a66c4608589611228 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Wed, 1 Jul 2026 10:51:59 +0200 Subject: [PATCH] arch(bff): ADR-0010 BFF OIDC validation + downstream boundaries (refs #8, #63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BFF is the portals' only backend (§8.3): it validates Keycloak digid-realm JWTs on POST /self-service/registrations (extracting bsn → domain), leaves GET /openbaar/register anonymous (public lookup, S-09), and fans out to the domain and projection over typed HTTP clients. Tests mint tokens with a test signing key; real Keycloak validation is a live-stack verify-bff check. Records the container OIDC issuer-mismatch wrinkle. OpenAPI is generated + committed for the S-08 client. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/architecture/adr-0010-bff-oidc.md | 74 ++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 75 insertions(+) create mode 100644 docs/architecture/adr-0010-bff-oidc.md diff --git a/docs/architecture/adr-0010-bff-oidc.md b/docs/architecture/adr-0010-bff-oidc.md new file mode 100644 index 0000000..d812298 --- /dev/null +++ b/docs/architecture/adr-0010-bff-oidc.md @@ -0,0 +1,74 @@ +# ADR-0010: The BFF validates Keycloak tokens and is the portals' only backend + +- **Status:** Accepted +- **Date:** 2026-07-01 +- **Deciders:** Respellion engineering +- **Relates to:** S-07 (#8); proposal #63; builds on ADR-0001 (loose coupling, §8.3), S-02 (#3, Keycloak realms), S-05 (#6, Domain Service), S-06 (#7, read projection) + +## Context + +S-07 (#8) adds the **BFF (Backend-for-Frontend)** — the single backend the Angular portals talk +to (CLAUDE.md §8.3). For the walking skeleton it exposes two endpoints and fans out to services +already built: + +- `POST /self-service/registrations` → Domain Service `POST /registrations` (S-05). +- `GET /openbaar/register?q=…` → projection-api `GET /register` (S-06). + +It must validate tokens issued by Keycloak (S-02). This is an ADR-worthy moment (§14): a new +dependency (JWT bearer authentication) and two new service boundaries (BFF→domain, BFF→projection). + +## Decision + +**The BFF is the portals' only backend; it validates Keycloak `digid`-realm JWTs on the +self-service endpoint, leaves the openbaar lookup anonymous, and fans out to the domain and +projection over typed HTTP clients.** + +- **Auth model.** `POST /self-service/registrations` requires a valid `digid`-realm bearer token; + the BFF reads the `bsn` claim and forwards it to the domain. Missing / invalid / expired token → + **401**. `GET /openbaar/register` is **anonymous** — the openbaar register is a public lookup + (S-09), so no token is required. +- **Portals talk only to the BFF (§8.3).** They never call the Domain Service, ACL, projection, or + OpenZaak directly. The BFF orchestrates via typed `HttpClient`s whose base URLs come from config. + Downstream calls are unauthenticated on the internal network for the walking skeleton; a + service-to-service auth story (e.g. client-credentials) is a later slice, not this one. +- **Validation is `Microsoft.AspNetCore.Authentication.JwtBearer`** pointed at the Keycloak `digid` + realm authority. **New dependency justification:** it gives us standards-based OIDC/JWT validation + (signature, issuer, expiry, audience) maintained by the framework; rolling our own JWT validation + would be error-prone security code; the risk is a first-party ASP.NET Core package — minimal. +- **Tests mint their own tokens.** `WebApplicationFactory` tests override the bearer options with a + **test signing key**, so valid / invalid / expired tokens are minted in-process without a live + Keycloak. Real Keycloak validation is exercised by a live-stack `verify-bff` check. +- **OpenAPI is generated and committed** (`services/bff/openapi.json`) from .NET's built-in OpenAPI, + so S-08's Angular client is generated from the spec, never hand-written (§10). + +## Known wrinkle — container OIDC issuer mismatch + +Keycloak stamps tokens with an `iss` equal to its **browser-facing** URL (what the portal used to +log in), which differs from the BFF's **in-container** authority (`http://keycloak:8080/realms/digid`). +Strict issuer validation then rejects otherwise-valid tokens. Unit tests avoid this (test key). +`verify-bff` handles it by aligning the configured authority/issuer with the token's `iss` (and, if +needed, disabling metadata address rewriting). Recorded so it is not rediscovered each time. + +## Consequences + +- **Positive:** the walking skeleton gains its front door; §8.3 holds with all portal traffic going + through one backend; token validation is standard and testable without infra; the committed + OpenAPI unblocks S-08. +- **Negative / deferred:** + - Downstream service-to-service auth is deferred (internal-network trust for now). + - The openbaar endpoint is anonymous; when public-safe field filtering tightens (S-09) it stays + anonymous but the projection query narrows. + - The issuer-mismatch handling is dev-oriented; a production reverse-proxy setup would align the + browser and internal issuer URLs instead. + +## Alternatives considered + +- **Token-gate the openbaar endpoint too** — rejected: the openbaar register is public by design + (S-09); requiring a login would contradict the slice's intent. +- **Validate tokens by calling Keycloak's introspection endpoint per request** — rejected: adds a + network hop per call and a Keycloak dependency on the hot path; local JWT signature validation via + the realm's JWKS is the standard, faster choice. +- **Hand-written JWT parsing** — rejected: security-sensitive code we shouldn't own when a + first-party validator exists. +- **Generate the OpenAPI client by hand / keep the spec uncommitted** — rejected: §10 requires a + generated client from a committed spec. diff --git a/mkdocs.yml b/mkdocs.yml index 73a9a55..51895e5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,6 +31,7 @@ nav: - "ADR-0007: OpenZaak → NRC notification wiring": architecture/adr-0007-notification-wiring.md - "ADR-0008: Read projection store": architecture/adr-0008-read-projection-store.md - "ADR-0009: External-task job worker": architecture/adr-0009-external-task-job-worker.md + - "ADR-0010: BFF OIDC validation": architecture/adr-0010-bff-oidc.md - Working in Gitea: gitea-workflow.md - Demo script: demo-script.md - Runbooks: