Context
Raised by @hamedmishian (TU Dresden, Institut für Medizinische Informatik und Biometrie) who is deploying Proxy Smart alongside a separate Next.js clinical application (ophthalmic demo app), both sharing one Keycloak instance behind nginx on separate subdomains.
Related projects:
Problem
In the current single-realm architecture, Keycloak's SSO causes session leakage across co-deployed applications. When a doctor logs into the clinical app, the shared SSO cookie automatically authenticates them in the Proxy Smart admin UI — no credentials prompt, no way to block it at the frontend level.
Hamed's workaround: split Keycloak into two realms (admin realm for Proxy Smart, clinical realm for SMART apps), with a service account bridge for cross-realm admin operations.
Security Gaps Found
- No issuer verification —
jwt.verify() had no issuer constraint. Any token signed by the configured realm's keys was accepted.
- Stale JWKS client — JWKS client was created once at module load; reconfiguring the realm via
/admin/keycloak-config left it pointing at the old realm's keys.
- Missing SMART
aud validation — SMART App Launch 2.2.0 requires the resource server to validate the aud parameter from the authorization request matches its own FHIR endpoint. This was not implemented, nor was aud even in the authorization query schema.
Industry Best Practice: Single-Realm Multi-Tenancy
Research of the Keycloak ecosystem shows the community strongly favors single-realm models:
- Keycloak 26.x Organizations (GA) — native multi-tenancy within a single realm: per-org IdPs, per-org auth flows, org-scoped tokens
- p2-inc/keycloak-orgs (526★) — explicitly tried and rejected "one realm per tenant" due to scale/frailty trade-offs
- anarsultanov/keycloak-multi-tenancy (171★) — tenants as entities within a single realm
Realm-per-tenant becomes fragile as apps grow (3rd app = 3rd realm or more complex routing), and cross-realm admin requires service account bridges that add operational burden.
Changes
Phase 1: Token Validation Hardening ✅
Phase 2: SMART aud Parameter Validation ✅
Note: OIDC aud claim validation (the JWT aud field) was evaluated and intentionally skipped — Keycloak sets aud to "account" by default, not a meaningful value. Custom audiences require configuring a Keycloak protocol mapper per client. The SMART aud validation above addresses the actual spec requirement.
Phase 3: Documentation
Context
Raised by @hamedmishian (TU Dresden, Institut für Medizinische Informatik und Biometrie) who is deploying Proxy Smart alongside a separate Next.js clinical application (ophthalmic demo app), both sharing one Keycloak instance behind nginx on separate subdomains.
Related projects:
Problem
In the current single-realm architecture, Keycloak's SSO causes session leakage across co-deployed applications. When a doctor logs into the clinical app, the shared SSO cookie automatically authenticates them in the Proxy Smart admin UI — no credentials prompt, no way to block it at the frontend level.
Hamed's workaround: split Keycloak into two realms (admin realm for Proxy Smart, clinical realm for SMART apps), with a service account bridge for cross-realm admin operations.
Security Gaps Found
jwt.verify()had noissuerconstraint. Any token signed by the configured realm's keys was accepted./admin/keycloak-configleft it pointing at the old realm's keys.audvalidation — SMART App Launch 2.2.0 requires the resource server to validate theaudparameter from the authorization request matches its own FHIR endpoint. This was not implemented, nor wasaudeven in the authorization query schema.Industry Best Practice: Single-Realm Multi-Tenancy
Research of the Keycloak ecosystem shows the community strongly favors single-realm models:
Realm-per-tenant becomes fragile as apps grow (3rd app = 3rd realm or more complex routing), and cross-realm admin requires service account bridges that add operational burden.
Changes
Phase 1: Token Validation Hardening ✅
expectedIssuergetter toconfig.keycloak(match against configured realm's issuer URL)issuerverification tojwt.verify()inauth.ts/admin/keycloak-configreconfiguration)pdf-extract-opendataloader.test.ts(was causing 100 test failures viamock.modulepoisoning)Phase 2: SMART
audParameter Validation ✅aud,resource(RFC 8707 synonym), andlaunchparams toAuthorizationQueryschemaaudat/authorizeendpoint against configured FHIR server endpoints400 invalid_requestifauddoesn't match any known FHIR endpointPhase 3: Documentation