openova/platform/keycloak
e3mrah 255eb3bf17
feat(sandbox+auth+newapi): Wave 1b — newapi proxy + BYOS + org-scoped JWT (#1619)
Three coordinated deliverables for Sandbox Wave 1b — scaffolding +
design + the ONE prerequisite (long-lived org-scoped JWT) the rest of
Sandbox depends on.

Deliverable 1 — newapi proxy contract:
  - products/sandbox/docs/newapi-proxy-contract.md: agent-pod env
    (LLM_GATEWAY_URL / OPENAI_BASE_URL alias), provider selection
    (?provider=qwen; default Qwen via omtd.bankdhofar.com), per-Sandbox
    token issuance via /admin/tokens/sandbox bridge, lifecycle +
    rotation, auth model.
  - platform/newapi/internal/handler/sandbox_token.go: bridge handler
    stub. Validates the inbound PAT (typ=pat + aud=newapi + org_id
    cross-check vs request body), then echoes a NewAPI-shaped response
    so the contract is testable without the upstream NewAPI admin
    API. Wave 4 wires the actual upstream calls.

Deliverable 2 — Claude Code BYOS OAuth:
  - products/sandbox/docs/claude-code-byos.md: UX (Connect Claude Max →
    OAuth → refresh token Secret/catalyst-system/sandbox-byos-claude-
    code-<user-uid>), Pod env injection (ANTHROPIC_API_KEY bypassing
    newapi), per-session toggle, revocation paths, chart wiring.
  - products/catalyst/bootstrap/api/internal/handler/byos_claude_code.go:
    POST /start, GET /callback, DELETE, GET /status — four endpoints
    behind RequireSession. Honest 503 + 501 surface so the popup
    flow exercises end-to-end against the placeholder client_id;
    Wave 4 flips it live.

Deliverable 3 — Long-lived org-scoped JWT (THE prerequisite):
  - platform/keycloak/chart/templates/configmap-sovereign-realm.yaml +
    configmap-tenant-realm.yaml: add `org` protocolMapper emitting
    user attribute `org` as claim `org_id`; add `org` to default
    client scopes for ALL clients.
  - core/services/auth/handlers/handlers.go: include typ=session in
    JWTs + document the cross-service claim contract.
  - core/services/auth/handlers/pat.go: NEW POST /auth/pat with
    admin-configurable TTL (default 7d, max 90d), audience claim,
    capabilities pass-through, typ=pat discriminator.
  - core/services/auth/handlers/routes.go + main.go: wire /auth/pat
    behind JWTAuth middleware.
  - core/services/shared/auth/claims.go: single Claims struct +
    HasCapability/HasGroup helpers + ContextKey for cross-service
    consumers (sandbox-controller, newapi bridge, MCP server).
  - products/catalyst/bootstrap/api/internal/auth/session.go: align
    Org JSON tag with new `org_id` claim; UnmarshalJSON accepts BOTH
    legacy `org` and new `org_id` so a rolling chart upgrade does
    not regress org-scoped queries.

Out of scope (Wave 4 wires):
  - Sandbox CRD + controller (writes Secret, mounts Pod env).
  - Actual outbound HTTP to Anthropic /oauth/token + KMS encrypt.
  - Actual outbound HTTP to NewAPI admin API.
  - Per-Sandbox capability projection from Keycloak groups.
  - PAT revocation lookup (jti store) + /auth/pats list.
  - Settings UI card + session-toolbar routing toggle.

Build verification (go vet + go build clean):
  - core/services/auth/...
  - core/services/shared/...
  - platform/newapi/internal/handler/...
  - products/catalyst/bootstrap/api/...

Founder TODO (single knob to flip BYOS live, Wave 4):
  Register an Anthropic OAuth client at
  https://console.anthropic.com/settings/oauth (public PKCE,
  redirect=https://console.<sov-fqdn>/api/v1/sandbox/byos/claude-code/callback)
  and paste the client_id into clusters/<sovereign>/bootstrap-kit/
  sandbox.yaml. Today every BYOS endpoint returns 503 with a clear
  message pointing at claude-code-byos.md §8.

Refs: products/sandbox/docs/architecture.md §6 (THE prerequisite).

Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.com>
2026-05-18 08:43:11 +04:00
..
chart feat(sandbox+auth+newapi): Wave 1b — newapi proxy + BYOS + org-scoped JWT (#1619) 2026-05-18 08:43:11 +04:00
blueprint.yaml fix(keycloak): parameterize realm name (target-state realm-per-Sovereign) — qa-loop iter-12 Fix #53A (#1271) 2026-05-10 10:48:09 +04:00
README.md docs(pass-34): banned-term TENANT sweep + keycloak hostname drift 2026-04-27 22:42:50 +02:00

Keycloak

User identity for Catalyst Sovereigns. Per-Sovereign supporting service in the Catalyst control plane (see docs/PLATFORM-TECH-STACK.md §2.3). Also serves as the FAPI Authorization Server for the Fingate (Open Banking) Blueprint.

Status: Accepted | Updated: 2026-04-27

Catalyst topology (set at Sovereign provisioning time, see docs/SECURITY.md §6):

  • per-organization (SME-style Sovereigns, e.g. omantel): one minimal Keycloak per Organization (single replica, embedded H2/sqlite, ~150 MB RAM, no HA). Blast radius limited to one Org.
  • shared-sovereign (corporate self-host, e.g. bankdhofar): one HA Keycloak for the entire Sovereign with multiple realms (one per Organization), federating to the corporation's identity provider (Azure AD, Okta).

Overview

Keycloak provides:

  • User identity for the Catalyst console, marketplace, admin, REST/GraphQL API, and per-Application SSO.
  • OIDC / OAuth 2.0 / SAML federation to corporate IdPs.
  • FAPI 2.0 compliant authorization for the Fingate Open Banking Blueprint:
    • PSD2/FAPI 2.0 certification path
    • eIDAS certificate validation
    • Consent management
    • Multi-tenant TPP support (PSD2 sense — Third Party Providers, not platform tenants)

Architecture

flowchart TB
    subgraph Keycloak["Keycloak"]
        Core[Core IAM]
        FAPI[FAPI Module]
        Consent[Consent Service]
    end

    subgraph Backend["Backend"]
        CNPG[CNPG Postgres]
    end

    subgraph Integration["Integration"]
        Envoy[Envoy/Cilium]
        TPP[TPP Registry]
    end

    Envoy -->|"ext_authz"| FAPI
    FAPI --> Consent
    Core --> CNPG
    FAPI --> TPP

FAPI 2.0 Compliance

Feature Status
PKCE Required
Signed JWT requests Required
mTLS client auth Required
PAR (Pushed Authorization) Required
JARM responses Required

Configuration

Keycloak Deployment

The deployment shape depends on Catalyst's keycloakTopology choice (see banner above):

Corporate (shared-sovereign) — one HA Keycloak per Sovereign in catalyst-keycloak namespace on the management cluster:

apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
  name: keycloak
  namespace: catalyst-keycloak
spec:
  instances: 3                          # HA, multiple replicas
  db:
    vendor: postgres
    host: keycloak-postgres-rw.catalyst-keycloak.svc
    port: 5432
    database: keycloak
    usernameSecret:                     # ESO-managed via OpenBao
      name: keycloak-db-credentials
      key: username
    passwordSecret:
      name: keycloak-db-credentials
      key: password
  http:
    tlsSecret: keycloak-tls
  hostname:
    hostname: auth.<location-code>.<sovereign-domain>

SME (per-organization) — one minimal Keycloak per Organization in the Org's namespace on the management cluster:

apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
  name: keycloak
  namespace: <org>                     # per-Org namespace
spec:
  instances: 1                          # no HA at SME tier
  db:
    vendor: postgres                    # or H2/sqlite for the smallest tier
    host: keycloak-postgres-rw.<org>.svc
    port: 5432
    database: keycloak
    # ... credentials
  hostname:
    hostname: auth.<org>.<location-code>.<sovereign-domain>

FAPI Realm Configuration

{
  "realm": "open-banking",
  "enabled": true,
  "sslRequired": "all",
  "attributes": {
    "fapi.compliance.mode": "strict",
    "pkce.required": "S256",
    "require.pushed.authorization.requests": "true"
  },
  "clientPolicies": {
    "policies": [
      {
        "name": "fapi-advanced",
        "enabled": true,
        "conditions": [
          {
            "condition": "client-roles",
            "configuration": {
              "roles": ["fapi-client"]
            }
          }
        ],
        "profiles": ["fapi-2-security-profile"]
      }
    ]
  }
}

eIDAS Certificate Validation

TPP certificates are validated against qualified trust service providers:

apiVersion: v1
kind: ConfigMap
metadata:
  name: eidas-config
  namespace: open-banking
data:
  trust-anchors: |
    # QTSPs for eIDAS validation
    - name: qualified-tsp-1
      certificate: |
        -----BEGIN CERTIFICATE-----
        ...
        -----END CERTIFICATE-----    

TPP Client Registration

{
  "clientId": "tpp-12345",
  "clientAuthenticatorType": "client-jwt",
  "redirectUris": ["https://tpp.example.com/callback"],
  "attributes": {
    "tpp.authorization.number": "PSDGB-FCA-123456",
    "tpp.eidas.certificate": "...",
    "tpp.roles": ["AISP", "PISP"]
  },
  "defaultClientScopes": [
    "openid",
    "accounts",
    "payments"
  ]
}

sequenceDiagram
    participant TPP
    participant Keycloak
    participant User
    participant ConsentService

    TPP->>Keycloak: PAR request
    Keycloak->>TPP: request_uri
    TPP->>User: Redirect to Keycloak
    User->>Keycloak: Authenticate
    Keycloak->>ConsentService: Get consent page
    ConsentService->>User: Show accounts/permissions
    User->>Keycloak: Grant consent
    Keycloak->>ConsentService: Store consent
    Keycloak->>TPP: Authorization code

High Availability

HA shape depends on Catalyst's keycloakTopology:

  • shared-sovereign (corporate): 3 replicas behind a Service, CNPG PostgreSQL with WAL streaming to async standby, session replication via Infinispan.
  • per-organization (SME): single replica, no session replication, restart-on-deploy is acceptable for SME-tier SLAs. Larger SMEs can opt into HA via tier upgrade — same Catalyst CR shape, just bumped instances.

Part of OpenOva