Skip to content

feat(compose): add Keycloak auth mode to dev-compose stack#1821

Merged
ktursunov merged 1 commit into
constructorfabric:mainfrom
ktursunov:feat/keycloak-dev-compose-auth-mode
Jul 18, 2026
Merged

feat(compose): add Keycloak auth mode to dev-compose stack#1821
ktursunov merged 1 commit into
constructorfabric:mainfrom
ktursunov:feat/keycloak-dev-compose-auth-mode

Conversation

@ktursunov

@ktursunov ktursunov commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Adds an AUTH_MODE (fakeidp default | keycloak) to the docker-compose dev stack so local runs can use a real Keycloak login — a login form, real tokens, and custom claims — instead of the fakeidp bypass. Default (fakeidp) behavior is unchanged.

Why

fakeidp is great for fast, no-login dev, but it can't exercise the real OIDC login form, real token claims, or admin-console behavior. Keycloak mode gives a customer-IdP-like experience locally while keeping the stack lightweight (a single container, embedded H2, --import-realm).

How

  • deploy/compose/keycloak/gen-realm.py — generates the Keycloak insight realm from the seed roster (deploy/seed/profiles.py) so login personas match the seeded org: 25 users, clients insight + insight-authenticator, custom-claim mappers (tenant_id/org_unit/groups/roles/aud), CEO team=None → "executive". A --dev-email arg decouples the roster anchor from VITE_DEV_USER_EMAIL.
  • docker-compose.ymlkeycloak service behind profile auth-keycloak; fakeidp moved behind auth-fakeidp; authenticator.depends_on.fakeidp gains required: false (so --auth=keycloak doesn't fail on the inactive profile); KC_HOSTNAME includes /kc so the advertised issuer is http://keycloak:8085/kc/realms/insight.
  • dev-compose.sh--auth=fakeidp|keycloak flag + AUTH_MODE (mirrors FRONTEND_MODE). Keycloak branch generates the realm before up, repoints the authenticator OIDC env, warns on a missing /etc/hosts entry, stops the other mode's IdP on an in-place switch, and disables frontend dev-impersonation via a per-service override (scoped so the seed step and roster keep VITE_DEV_USER_EMAIL).
  • deploy/compose/insight-init.sh — first-run wizard prompts for the auth mode.
  • Docs — canonical deploy/compose/keycloak/README.md, a distinct CONTRIBUTING "Local dev auth backend" section (the api-gateway real-OIDC section is left untouched), a DESIGN AUTH_MODE row, a fakeidp README pointer, and a .env.compose.example stanza.

One-time setup for keycloak mode

Add to /etc/hosts (browser + containers must resolve Keycloak at the same host):

127.0.0.1  keycloak

Then: ./dev-compose.sh up --auth=keycloak — log in as any seeded persona (email_*@company.nonpresent / insight-dev); admin console http://keycloak:8085/kc/admin/ (admin/admin).

Test plan (verified locally, both modes, with seeding)

  • Stack builds (native arm64) + first-run seed: 25 persons + 28,881 silver rows + gold layer built — no drift against current main's git-silver models.
  • fakeidp: stack up, caller resolves via gateway (HTTP 200), org chart intact.
  • keycloak: keycloak up / fakeidp down, issuer http://keycloak:8085/kc/realms/insight, direct-grant token carries tenant_id/org_unit/groups/roles/aud=insight, dev lead logs in as a real Keycloak user, caller resolves (HTTP 200).
  • Mode switching (fakeidp ⇄ keycloak via up): only the active IdP stays running.
  • docker compose config valid for both auth-fakeidp and auth-keycloak.

Notes / scope

  • The compose api-gateway runs no-auth.yaml (auth_disabled) in both modes by design — keycloak mode delivers real login + real tokens/claims at the authenticator; making the gateway enforce the token is a separate change (the production OIDC swap).
  • Rebased on current main (incl. the bootstrap-db and git-silver changes); no conflicts, no script changes required.

Summary by CodeRabbit

  • New Features
    • Added selectable local authentication for the Compose dev stack (--auth=fakeidp|keycloak), with Fake IdP as the default.
    • Keycloak mode now brings real login via the bundled Keycloak container, includes seeded dev users, and exposes an admin console; service URLs and startup behavior are auth-aware.
    • Added an interactive auth choice to the compose init flow.
  • Bug Fixes
    • Improved Compose startup so the stack can run without the non-selected IdP profile.
  • Documentation
    • Documented AUTH_MODE/--auth behavior, Keycloak setup, and the Keycloak token-claims/custom-claims contract.

@ktursunov
ktursunov requested a review from a team as a code owner July 17, 2026 17:28
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ktursunov, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99289444-b1ca-4070-8a0e-86f182ce7043

📥 Commits

Reviewing files that changed from the base of the PR and between 7c93e10 and 509e0ff.

📒 Files selected for processing (12)
  • .cf-studio/config/AGENTS.md
  • .env.compose.example
  • .gitignore
  • CONTRIBUTING.md
  • deploy/compose/insight-init.sh
  • deploy/compose/keycloak/README.md
  • deploy/compose/keycloak/api-gateway.keycloak.yaml
  • deploy/compose/keycloak/gen-realm.py
  • dev-compose.sh
  • docker-compose.yml
  • docs/components/deployment/specs/DESIGN.md
  • src/backend/services/fakeidp/README.md
📝 Walkthrough

Walkthrough

The compose development stack now supports fakeidp and Keycloak authentication modes. It adds Keycloak realm generation, auth-aware Compose profiles and lifecycle handling, interactive setup persistence, and documentation for local OIDC configuration.

Changes

Compose authentication modes

Layer / File(s) Summary
Keycloak realm generation
deploy/compose/keycloak/gen-realm.py
Generates the insight realm JSON with seeded users, groups, roles, clients, credentials, and custom token mappers.
Compose authentication runtime
dev-compose.sh, docker-compose.yml, src/backend/services/api-gateway/config/keycloak.yaml
Adds --auth parsing, profile selection, Keycloak startup configuration, fakeidp shutdown handling, optional fakeidp dependency behavior, enforced gateway authentication, and mode-specific service URLs.
Auth mode setup and persistence
.env.compose.example, deploy/compose/insight-init.sh, .gitignore
Documents and persists AUTH_MODE, prompts for the backend during setup, checks the Keycloak hostname alias, and ignores generated realm output.
Authentication configuration documentation
.cf-studio/config/AGENTS.md, CONTRIBUTING.md, deploy/compose/keycloak/README.md, docs/components/deployment/specs/DESIGN.md, src/backend/services/fakeidp/README.md
Documents supported modes, OIDC flow, local setup, login behavior, claims, credentials, and realm generation rules.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Frontend
  participant Authenticator
  participant Keycloak
  participant Redis
  participant Gateway
  Frontend->>Authenticator: request authentication
  Authenticator->>Keycloak: authorize with OIDC
  Keycloak-->>Authenticator: authorization code
  Authenticator->>Redis: store authenticated session
  Gateway->>Authenticator: exchange session cookie
  Authenticator-->>Gateway: short-lived gateway JWT
Loading

Suggested reviewers: aleksdotbar

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a Keycloak auth mode to the dev-compose stack.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
deploy/compose/keycloak/gen-realm.py (1)

211-213: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Specify UTF-8 encoding when writing the JSON file.

Path.write_text() relies on the system default encoding (which can be CP1252 on Windows) if not explicitly specified. Specifying encoding="utf-8" ensures the JSON artifact is reliably written as UTF-8, preventing decoding errors in Keycloak if the seeded roster contains Unicode characters.

♻️ Proposed fix
     out_path = Path(args.out)
     out_path.parent.mkdir(parents=True, exist_ok=True)
-    out_path.write_text(json.dumps(realm, indent=2, sort_keys=True) + "\n")
+    out_path.write_text(json.dumps(realm, indent=2, sort_keys=True) + "\n", encoding="utf-8")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/compose/keycloak/gen-realm.py` around lines 211 - 213, Update the
Path.write_text call in the output-writing flow to explicitly use UTF-8
encoding, ensuring the generated realm JSON is consistently encoded across
platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/compose/keycloak/README.md`:
- Around line 3-5: Update the README’s BFF description and authentication
diagram to show the browser redirecting to Keycloak’s authorization endpoint and
then returning to the authenticator callback. Clarify that the browser does not
receive or manage IdP tokens, while the authenticator retains them server-side,
and remove the claim that the browser never communicates with the IdP.

---

Nitpick comments:
In `@deploy/compose/keycloak/gen-realm.py`:
- Around line 211-213: Update the Path.write_text call in the output-writing
flow to explicitly use UTF-8 encoding, ensuring the generated realm JSON is
consistently encoded across platforms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 261cc9b9-4a11-42eb-8ea8-25049dc14ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 37c870f and beb7040.

📒 Files selected for processing (11)
  • .cf-studio/config/AGENTS.md
  • .env.compose.example
  • .gitignore
  • CONTRIBUTING.md
  • deploy/compose/insight-init.sh
  • deploy/compose/keycloak/README.md
  • deploy/compose/keycloak/gen-realm.py
  • dev-compose.sh
  • docker-compose.yml
  • docs/components/deployment/specs/DESIGN.md
  • src/backend/services/fakeidp/README.md

Comment thread deploy/compose/keycloak/README.md Outdated
@ktursunov
ktursunov force-pushed the feat/keycloak-dev-compose-auth-mode branch 3 times, most recently from 7dbbe7c to 4663427 Compare July 17, 2026 18:20
@ktursunov
ktursunov enabled auto-merge July 17, 2026 18:29
@ktursunov
ktursunov force-pushed the feat/keycloak-dev-compose-auth-mode branch 3 times, most recently from 7c93e10 to 558f3df Compare July 18, 2026 04:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/services/api-gateway/config/keycloak.yaml`:
- Around line 14-20: Remove the obsolete /etc/hosts instruction that adds
127.0.0.1 keycloak from deploy/compose/insight-init.sh, while preserving the
split-horizon issuer_url and jwks_url configuration documented in
src/backend/services/api-gateway/config/keycloak.yaml. Also update the
corresponding stale guidance in .env.compose.example; both sites require direct
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ecca940-ce25-4f0b-bd2a-a8da48229bbd

📥 Commits

Reviewing files that changed from the base of the PR and between beb7040 and 7c93e10.

📒 Files selected for processing (12)
  • .cf-studio/config/AGENTS.md
  • .env.compose.example
  • .gitignore
  • CONTRIBUTING.md
  • deploy/compose/insight-init.sh
  • deploy/compose/keycloak/README.md
  • deploy/compose/keycloak/gen-realm.py
  • dev-compose.sh
  • docker-compose.yml
  • docs/components/deployment/specs/DESIGN.md
  • src/backend/services/api-gateway/config/keycloak.yaml
  • src/backend/services/fakeidp/README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • .gitignore
  • .cf-studio/config/AGENTS.md
  • src/backend/services/fakeidp/README.md
  • docs/components/deployment/specs/DESIGN.md
  • CONTRIBUTING.md
  • docker-compose.yml
  • dev-compose.sh

Comment on lines +14 to +20
# Split-horizon (no /etc/hosts needed):
# - issuer_url = the PUBLIC browser-facing issuer (localhost:8085). It must
# equal the token `iss`, which the browser SPA obtains over the published
# port.
# - jwks_url = fetched by the gateway over the compose network at
# keycloak:8085 (Keycloak runs with KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true so
# it serves the certs endpoint on the internal host).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if insight-init.sh still contains the obsolete /etc/hosts check
cat -n deploy/compose/insight-init.sh | grep -A 10 -B 2 'keycloak mode needs a hosts alias'

# Check if README.md still mentions /etc/hosts
rg -n '/etc/hosts' deploy/compose/keycloak/README.md || true

Repository: constructorfabric/insight

Length of output: 804


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== keycloak.yaml ==\n'
cat -n src/backend/services/api-gateway/config/keycloak.yaml | sed -n '1,80p'

printf '\n== .env.compose.example ==\n'
cat -n .env.compose.example | sed -n '40,70p'

printf '\n== README mentions /etc/hosts ==\n'
rg -n '/etc/hosts|keycloak mode needs a hosts alias|127\.0\.0\.1\s+keycloak' deploy/compose/keycloak/README.md deploy/compose/insight-init.sh .env.compose.example src/backend/services/api-gateway/config/keycloak.yaml || true

Repository: constructorfabric/insight

Length of output: 5543


Remove the stale /etc/hosts hint deploy/compose/insight-init.sh:447-451 still tells users to add 127.0.0.1 keycloak, but this split-horizon config already uses localhost:8085 externally and keycloak:8085 internally.

📍 Affects 2 files
  • src/backend/services/api-gateway/config/keycloak.yaml#L14-L20 (this comment)
  • .env.compose.example#L51-L51
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/api-gateway/config/keycloak.yaml` around lines 14 - 20,
Remove the obsolete /etc/hosts instruction that adds 127.0.0.1 keycloak from
deploy/compose/insight-init.sh, while preserving the split-horizon issuer_url
and jwks_url configuration documented in
src/backend/services/api-gateway/config/keycloak.yaml. Also update the
corresponding stale guidance in .env.compose.example; both sites require direct
changes.

@ktursunov
ktursunov force-pushed the feat/keycloak-dev-compose-auth-mode branch 3 times, most recently from 5046a35 to a17f5ed Compare July 18, 2026 05:21
… enforce)

Add an AUTH_MODE (fakeidp default | keycloak) to the docker-compose dev stack:
- fakeidp = auth BYPASS (no login; api-gateway runs no-auth.yaml; the frontend
  supplies an impersonated identity). Default; unchanged behavior.
- keycloak = auth ENFORCED (real login form + the api-gateway validates the
  Keycloak JWT on every request).

Configured via AUTH_MODE in .env.compose (like FRONTEND_MODE); a per-run --auth
flag overrides it. No /etc/hosts change is needed.

How keycloak mode works:
- Keycloak: single container, embedded H2, --import-realm; realm generated from
  the seed roster (deploy/seed/profiles.py) so login personas match the seeded
  org. KC_HOSTNAME=http://localhost:8085/kc (+ KC_HOSTNAME_BACKCHANNEL_DYNAMIC)
  so the public issuer is localhost — reachable by the host browser via the
  published port, no hosts entry.
- Frontend: does the OIDC code+PKCE flow directly (SPA), public client `insight`;
  the ghcr image entrypoint injects window.__OIDC_CONFIG__ from OIDC_* env, so
  keycloak mode auto-switches FRONTEND_MODE to ghcr (Vite dev / plain-nginx built
  can't inject it).
- api-gateway: switched to a fully-resolved keycloak.yaml (auth_disabled:false)
  that validates iss=localhost:8085 but fetches JWKS over the compose network at
  keycloak:8085 (the orbstack-style issuer/JWKS split). A static file because
  compose mounts configs without Helm envsubst and gears doesn't interpolate
  ${ENV}; hyphenated APP__gears__ env keys are invalid.
- Custom claims tenant_id/org_unit/groups/roles/aud on both clients.

Files: deploy/compose/keycloak/gen-realm.py (seeder-driven realm; --dev-email;
CEO->executive; localhost:3000 redirect), src/backend/services/api-gateway/
config/keycloak.yaml (new, auth-enforcing), docker-compose.yml (keycloak service
+ auth-fakeidp/auth-keycloak profiles + fakeidp depends_on required:false),
dev-compose.sh (--auth/AUTH_MODE mirroring FRONTEND_MODE; per-run realm gen;
frontend-scoped impersonation-off + gateway keycloak.yaml via the generated
override; auto-switch to ghcr; stop the other IdP on an in-place switch),
deploy/compose/insight-init.sh (first-run wizard prompts for auth mode), docs
(keycloak README, CONTRIBUTING, DESIGN, .env.compose.example, fakeidp README).

Verified live, both modes with seeding (25 persons + silver/gold):
- fakeidp: no-auth gateway; no JWT required; caller resolves via the FE header.
- keycloak: JWKS loaded; no/invalid token -> 401; valid Keycloak token -> 200 +
  caller resolves; FE serves the localhost-issuer oidc-config; clean mode switch.

Signed-off-by: Konstantin Tursunov <Konstantin.Tursunov@constructor.tech>
@ktursunov
ktursunov force-pushed the feat/keycloak-dev-compose-auth-mode branch from a17f5ed to 509e0ff Compare July 18, 2026 05:27
@ktursunov
ktursunov merged commit 5ab0caa into constructorfabric:main Jul 18, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants