From 46eab199ee8f11bc2ab47bb316a2ad480fe1d21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Stensrud?= <89834487+JorgenStensrud@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:56:31 +0200 Subject: [PATCH] Add forte-cli public device-code Keycloak client Add a shared public client `forte-cli` to the `forte` realm so downloaded skills (forte-drop first) can do RFC 8628 device-code login through the Auth Sidecar. Today no client in the realm has the device grant enabled, so the flow cannot start. Client (inline in forte-realm.json, imported verbatim by keycloak-config-cli): - publicClient: true, standardFlowEnabled: false, directAccessGrantsEnabled: false - attributes: oauth2.device.authorization.grant.enabled=true - no secret, no redirectUris/webOrigins, no k8s.secret.sync It has to go in the realm JSON because the self-service registrar hardcodes publicClient:false/standardFlowEnabled:true and drops attributes. Also add forte-cli to the cleanup CronJob's protected list (belt-and-braces; it does not match the UUID pattern anyway). Additive only: gitea/grafana/argocd and all other realm settings are unchanged. Keycloak is deployed only via the upc-dev overlay, which inherits base values, so this lands on id.forteapps.net. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QciXev3MtCxo3eomcfrDRW --- docs/DEVELOPER-GUIDE.md | 6 ++++++ infra/values/base/keycloak-values.yaml | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/DEVELOPER-GUIDE.md b/docs/DEVELOPER-GUIDE.md index 0e9f5aa..0dfb810 100644 --- a/docs/DEVELOPER-GUIDE.md +++ b/docs/DEVELOPER-GUIDE.md @@ -1469,6 +1469,12 @@ ArgoCD will sync the Keycloak config, and the registrar CronJob will pick up the | `k8s.secret.client-id-key` | No | `client-id` | Field name for the client ID in the K8s Secret | | `k8s.secret.client-secret-key` | No | `client-secret` | Field name for the client secret in the K8s Secret | +#### Public CLI Client (Device-Code Login) + +`forte-cli` is a shared **public** client (no secret) with the RFC 8628 device-authorization grant enabled (`oauth2.device.authorization.grant.enabled: "true"`, `standardFlowEnabled: false`, `directAccessGrantsEnabled: false`). Downloaded skills and CLI tools that log in through the Auth Sidecar (forte-drop first) use it with `_CLIENT_ID=forte-cli`; nothing per-tool needs to be registered in Keycloak. + +It must be defined in `forte-realm.json` (this legacy path): the self-service registrar hardcodes `publicClient: false` / `standardFlowEnabled: true` and drops `attributes`, so a `client-config` Secret cannot produce a public device-code client. It carries no `k8s.secret.sync` attribute (the registrar's secret sync skips it) and is listed in the cleanup CronJob's protected clients. + ### Retrieving Secrets for External Deployments The registrar always writes a **central copy** of every synced secret to the `secrets` namespace, in addition to the target namespace. This allows operators to retrieve client credentials for applications deployed outside this cluster: diff --git a/infra/values/base/keycloak-values.yaml b/infra/values/base/keycloak-values.yaml index 00a9459..c3cf15f 100644 --- a/infra/values/base/keycloak-values.yaml +++ b/infra/values/base/keycloak-values.yaml @@ -186,6 +186,21 @@ keycloakConfigCli: } } ] + }, + { + "clientId": "forte-cli", + "name": "Forte CLI", + "description": "Shared public client for RFC 8628 device-code login from downloaded skills/CLI tools (forte-drop first) against services behind Auth Sidecar. No client secret.", + "enabled": true, + "protocol": "openid-connect", + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "publicClient": true, + "redirectUris": [], + "webOrigins": [], + "attributes": { + "oauth2.device.authorization.grant.enabled": "true" + } } ], "browserFlow": "browser-auto-idp", @@ -668,7 +683,7 @@ extraDeploy: MIN_AGE_SEC=$((MIN_AGE_DAYS * 86400)) # Hardcoded protected clients (never delete these) - PROTECTED_JSON='["gitea","grafana","argocd","vaultwarden","account","account-console","admin-cli","broker","realm-management","security-admin-console"]' + PROTECTED_JSON='["gitea","grafana","argocd","forte-cli","vaultwarden","account","account-console","admin-cli","broker","realm-management","security-admin-console"]' echo "Fetching clients from realm '${REALM}'..." CLIENTS=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \