{"id":50795278,"url":"https://github.com/streamshub/keycloak-auth-demo","last_synced_at":"2026-06-12T14:01:31.384Z","repository":{"id":353582460,"uuid":"1218185419","full_name":"streamshub/keycloak-auth-demo","owner":"streamshub","description":"A demo deployment of the StreamsHub Stack using Keycloak Authorization Services","archived":false,"fork":false,"pushed_at":"2026-04-24T13:50:41.000Z","size":21207,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-24T15:42:13.999Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/streamshub.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-22T16:07:58.000Z","updated_at":"2026-04-24T13:50:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/streamshub/keycloak-auth-demo","commit_stats":null,"previous_names":["streamshub/keycloak-auth-demo"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/streamshub/keycloak-auth-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/streamshub%2Fkeycloak-auth-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/streamshub%2Fkeycloak-auth-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/streamshub%2Fkeycloak-auth-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/streamshub%2Fkeycloak-auth-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/streamshub","download_url":"https://codeload.github.com/streamshub/keycloak-auth-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/streamshub%2Fkeycloak-auth-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34247461,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-12T14:01:06.416Z","updated_at":"2026-06-12T14:01:31.364Z","avatar_url":"https://github.com/streamshub.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StreamsHub OAuth Demo\n\nA demo showing OAuth-based authentication and authorization across a Kafka streaming platform using Keycloak, Strimzi, and the StreamsHub Console.\n\n## Scenario\n\nAn e-commerce platform processes order data. Some topics contain **personally identifiable information (PII)** -- customer names, emails, addresses -- while others contain only aggregated, anonymized events.\n\nTwo users demonstrate the access control:\n\n| User | Role | Can see `pii.orders` | Can see `public.order-events` |\n|------|------|---------------------|------------------------------|\n| **Alice** (Data Analyst) | Full PII access | Yes | Yes |\n| **Bob** (Business Analyst) | Public data only | No (topic is invisible) | Yes |\n\nAuthorization is enforced at **three layers**:\n1. **Kafka broker** -- Keycloak Authorization Services via `KeycloakAuthorizer` controls who can produce/consume which topics\n2. **StreamsHub Console** -- RBAC rules control what the UI displays per user\n3. **Apicurio Registry** -- Envoy + Authorino proxy enforces per-group schema access via Keycloak Authorization Services\n\n## Architecture\n\n```\n                         ┌──────────────┐\n                         │   Keycloak   │\n                         │  (kafka-oauth│\n                         │    realm)    │\n                         └──────┬───────┘\n                                │ OIDC / Token Validation / AuthZ Services\n           ┌────────────────────┼─────────────────┐\n           │                    │                  │\n           ▼                    ▼                  ▼\n  ┌────────────┐  ┌────────────────────┐  ┌──────────────────┐\n  │  StreamsHub │  │   Kafka Broker     │  │ Apicurio Registry │\n  │   Console   │  │  (OAUTHBEARER +    │  │  (Envoy sidecar + │\n  │  (OIDC +   │  │   KeycloakAuthz)   │  │   Authorino proxy)│\n  │   RBAC)    │  └────────────────────┘  └──────────────────┘\n  └─────┬──────┘          ▲                        ▲\n        │ forwards        │ SASL/OAUTHBEARER       │ Bearer token\n        │ user token      │                        │\n        └─────────────────┘    ┌──────────┐        │\n                               │  Java    │────────┘\n                               │  Clients │  Avro serdes\n                               │ (OAuth)  │  + schema registry\n                               └──────────┘\n```\n\nWhen a user logs into the Console, their OIDC access token is forwarded to Kafka as a SASL/OAUTHBEARER credential. Kafka's `KeycloakAuthorizer` checks their grants in Keycloak -- alice sees all topics, bob sees only `public.*` topics.\n\nThe Java clients use Avro serialization with Apicurio Registry for schema management. All registry API calls go through the Envoy + Authorino proxy, which delegates authorization decisions to Keycloak.\n\n## Prerequisites\n\n- [minikube](https://minikube.sigs.k8s.io/docs/start/)\n- [kubectl](https://kubernetes.io/docs/tasks/tools/)\n- [JBang](https://www.jbang.dev/download/)\n- Java 21+ and Maven 3.9+\n- Docker or Podman\n\n## Quick Start\n\n### 1. Start minikube\n\n```bash\nminikube start --memory=6144 --cpus=4\nminikube addons enable ingress\n```\n\n### 2. Deploy the full stack\n\n```bash\njbang scripts/Setup.java\n```\n\nThis runs all three phases:\n- **Phase 1**: Strimzi operator, Console operator, cert-manager, Authorino operator, Keycloak (with realm auto-import + groups scope)\n- **Phase 2**: Kafka cluster (with OAuth listener + KeycloakAuthorizer), Console (with OIDC), topics\n- **Phase 3**: Build producer/consumer Java apps, load images into minikube, deploy\n\nTo rebuild and redeploy only the client apps (skipping infrastructure):\n```bash\njbang scripts/Setup.java --skip-infra\n```\n\n### 3. Start minikube tunnel\n\nIn a separate terminal:\n```bash\nminikube tunnel\n```\n\n### 4. Access the demo\n\n| Service | URL | Credentials |\n|---------|-----|-------------|\n| Console | `https://console.\u003cminikube-ip\u003e.nip.io` | alice / alice-password **or** bob / bob-password |\n| Apicurio Registry | `https://apicurio-registry.\u003cminikube-ip\u003e.nip.io` | alice / alice-password **or** bob / bob-password |\n| Keycloak Admin | `https://keycloak.\u003cminikube-ip\u003e.nip.io` | admin / admin |\n\n\u003e The IP is auto-detected from `minikube ip`. To override (e.g., for a remote cluster), set `NIP_IO_IP=\u003cip\u003e jbang scripts/Setup.java`.\n\nAccept the self-signed certificate warning on first visit to each service (Console, Keycloak, Apicurio Registry).\n\n**As Alice**: Log in and see both `pii.orders` (with customer names, emails, addresses) and `public.order-events`.\n\n**As Bob**: Log in and see only `public.order-events`. The `pii.orders` topic is completely invisible.\n\n### 5. View client logs\n\n```bash\nkubectl logs -f deployment/order-producer -n kafka\nkubectl logs -f deployment/order-consumer -n kafka\n```\n\n## Cleanup\n\n```bash\njbang scripts/Teardown.java\n```\n\n## Apicurio Registry Authorization Proxy\n\nApicurio Registry lacks native fine-grained authorization -- it only has three global roles (`sr-admin`, `sr-developer`, `sr-readonly`) with no per-artifact or per-group granularity. To enforce the same PII/public access boundary used in Kafka, the registry sits behind an **Envoy + Authorino proxy** that delegates every authorization decision to **Keycloak Authorization Services**.\n\n### How it works\n\n```\nBrowser/Client\n    │\n    ▼\n  Envoy (:8443)\n    │── /apis/* ──► Authorino (ext-authz gRPC)\n    │                   │\n    │                   ├─ 1. Validate JWT (Keycloak JWKS)\n    │                   ├─ 2. Extract group prefix from request path\n    │                   ├─ 3. Ask Keycloak: \"does this user have\n    │                   │     permission group:{prefix}#{scope}?\"\n    │                   └─ 4. Keycloak evaluates policies ──► yes/no\n    │\n    ├── allowed ──► Registry backend (:8080, localhost only)\n    │\n    └── /ui/* ────► Registry UI (:8888, no auth -- static assets)\n```\n\nAuthorino constructs a permission check from the request:\n- **Path** `/apis/registry/v3/groups/pii.orders/artifacts` → prefix `pii` → resource `group:pii`\n- **Method** `GET` → scope `read`\n- **Result**: asks Keycloak \"does this user have `group:pii#read`?\"\n\nKeycloak evaluates its role-based policies and returns a yes/no decision. Authorino enforces it. The registry itself has no auth configuration -- it trusts the proxy.\n\n### Authorization granularity: prefix-level matching\n\nResources in Keycloak are defined at the **topic name prefix level**, not per-topic:\n\n| Keycloak Resource | Covers | Alice (Data Analyst) | Bob (Business Analyst) |\n|-------------------|--------|---------------------|----------------------|\n| `group:pii` | `pii.orders`, `pii.payments`, `pii.*` | read + write | **denied** |\n| `group:public` | `public.order-events`, `public.*` | read + write | read only |\n\nThis means adding a new topic like `pii.payments` automatically inherits the PII access policy -- no Keycloak configuration changes needed.\n\n### Design trade-offs\n\n**Prefix-level vs per-topic authorization**\n\nThe proxy extracts the prefix from the artifact group name (everything before the first `.`) and checks permissions against a Keycloak resource named `group:{prefix}`. This is a deliberate simplification:\n\n- **Pro: No config changes when adding topics.** A new `pii.customer-profiles` topic gets protected automatically.\n- **Pro: Mirrors the Kafka authorization model**, which already uses `pii.*` and `public.*` wildcard patterns.\n- **Con: No per-topic differentiation within a prefix.** You cannot grant access to `pii.orders` but deny `pii.payments` -- it's all-or-nothing per prefix.\n\nTo support per-topic authorization, you would need to either:\n1. **Define per-topic resources in Keycloak** (e.g., `group:pii.orders`, `group:pii.payments`) and remove the `.split('.')[0]` from the Authorino CEL expression. This gives full granularity but requires a new Keycloak resource for each topic.\n2. **Use Keycloak's UMA Protection API** with `matchingUri=true` for URI-based wildcard matching. This supports flexible patterns but requires multiple HTTP calls per request (PAT acquisition, resource lookup, ticket exchange) -- which cannot be done from Authorino's embedded OPA evaluator (see below).\n\n**Proxy authorization vs embedded authorization**\n\n| Aspect | Kafka (embedded) | Registry (proxy) |\n|--------|-----------------|------------------|\n| Authorization | `KeycloakAuthorizer` inside the broker | Envoy + Authorino external proxy |\n| Denied topic visibility | **Invisible** -- absent from metadata | **Visible** -- listed but returns 403 on access |\n| Granularity | Per-topic with wildcards | Per-prefix (configurable) |\n| Latency | In-process | Extra network hop (Envoy → Authorino → Keycloak) |\n\nThe visibility difference is a fundamental limitation of proxy-based authorization: the proxy operates at the HTTP request level and cannot filter the contents of list responses. Bob can see that a `pii.orders` group exists in the registry, but gets 403 when trying to access it. In Kafka, `pii.orders` is absent from metadata responses entirely.\n\n**Authorino OPA limitations**\n\nAuthorino embeds OPA as a Go library. The embedded OPA does not support `http.send` -- calls fail silently with no error message. This means Rego policies cannot make outbound HTTP requests (to Keycloak or anywhere else). All external calls must go through Authorino's native metadata evaluators (HTTP, UMA, UserInfo), with Rego limited to evaluating the fetched data. This constraint shaped the single-call `response_mode=decision` approach used here.\n\n## How It Works\n\nSee [docs/implementation-plan.md](docs/implementation-plan.md) for the full design, including:\n- Keycloak realm configuration (users, clients, Authorization Services)\n- Kafka `type: custom` listener and authorizer setup\n- Console OIDC and RBAC configuration\n- Dual-URL hostname handling for minikube\n\n## Project Structure\n\n```\n├── clients/                    # Java producer/consumer apps (Maven + Fabric8)\n│   ├── common/                 # Shared utilities (TopicGroupStrategy)\n│   ├── order-producer/         # Writes PII + public Avro records via registry\n│   ├── order-consumer/         # Reads and deserializes Avro from both topics\n│   └── deploy/                 # Kubernetes Deployment manifests\n├── components/\n│   ├── keycloak/               # Keycloak deployment + realm JSON\n│   ├── authorino/              # Authorino Operator install\n│   ├── apicurio-registry/      # Registry + Envoy sidecar + Authorino proxy\n│   ├── cert-manager/           # CA issuer chain for TLS on all ingresses\n│   └── topics/                 # KafkaTopic resources\n├── overlays/oauth/\n│   ├── base/                   # Phase 1: operators + Keycloak + Authorino Operator\n│   └── stack/                  # Phase 2: Kafka + Console + Registry + AuthConfig\n├── scripts/\n│   ├── Setup.java              # JBang script for full setup (all 3 phases)\n│   └── Teardown.java           # JBang script for full teardown\n└── docs/\n    └── implementation-plan.md  # Full design document with rationale\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstreamshub%2Fkeycloak-auth-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstreamshub%2Fkeycloak-auth-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstreamshub%2Fkeycloak-auth-demo/lists"}