Project

General

Profile

Sub-Task #254

Updated by Bricklou 25 days ago

Support multi-provider OIDC configuration, config-file based, not DB-backed and not CRD-based (Kubestro is a standalone Postgres-backed server, no operator/controller infra exists, so a CRD would mean building watch/reconcile machinery just to read config at boot). 

 Pattern (used by Grafana, Loki, Gitea): unified structured config file (YAML), `client_secret` given as an explicit `${ENV_VAR_NAME}` placeholder resolved against (YAML) + environment variable overrides for secrets, loaded via `config-rs` or `figment`, following the process env at load time — matches the repo's existing config style (`Config::from_env` in `apps/server/cli/src/config.rs`, and `apps/server/api/src/app/context/*.rs`, both read `std::env::var` directly, no config-merging crate anywhere today). `Config::from_env` pattern (apps/server/cli/src/config.rs). 

 Kubernetes/GitOps split: 
 - ConfigMap holds the full provider structure (issuer, client_id, scopes, icon) *except* `client_secret` — fully git-committed, plaintext, PR-reviewable. 
 - Secret holds each provider's `client_secret`, injected as an env var per provider (works as-is with SOPS, Sealed Secrets, External Secrets Operator, Vault, etc — no format changes needed on the secret side). 
 - One mechanism: `client_secret` values are `${ENV_VAR_NAME}` placeholders, resolved against mechanism for overrides: `config-rs`'s `Environment::default().separator("__")`, e.g. `OIDC_PROVIDERS__GOOGLE__CLIENT_SECRET` overrides `oidc_providers.google.client_secret`. No `${VAR}` interpolation syntax in the process env at load time file itself — admin picks the var name explicitly per provider (not derived from the provider slug, which would break for hyphenated keys since env var names can't portably contain hyphens). one override mechanism, not two. 

 Format spec: 

 ```yaml 
 # config.yaml (ConfigMap, git-committed, no secrets) 
 oidc_providers: 
   google: 
     name: Google 
     issuer_url: https://accounts.google.com     # discovery doc = {issuer_url}/.well-known/openid-configuration 
     client_id: xxxxx.apps.googleusercontent.com 
     client_secret: "${OIDC_GOOGLE_CLIENT_SECRET}" 
     scopes: [openid, email, profile] 
     icon: google                                # slug into github.com/homarr-labs/dashboard-icons 

   keycloak: 
     name: Company SSO 
     issuer_url: https://sso.example.com/realms/kubestro 
     client_id: kubestro-dashboard 
     client_secret: "${OIDC_KEYCLOAK_CLIENT_SECRET}" 
     scopes: [openid, email] 
     icon_url: https://sso.example.com/icon.svg # override for providers not in the known icon set 
 ``` 

 Env vars (from Secret, per provider — names are admin-chosen, matching whatever each `client_secret` placeholder above references): provider): 

 ```bash 
 OIDC_GOOGLE_CLIENT_SECRET=GOCSPX-xxxxx OIDC_PROVIDERS__GOOGLE__CLIENT_SECRET=GOCSPX-xxxxx 
 OIDC_KEYCLOAK_CLIENT_SECRET=s3cr3t OIDC_PROVIDERS__KEYCLOAK__CLIENT_SECRET=s3cr3t 
 ``` 

 Deployment mounts the ConfigMap as `/etc/kubestro/config.yaml` (readOnly volume) and injects each provider's secret `client_secret` as an env var via `secretKeyRef`, using whichever var name that provider's `${...}` placeholder names. `secretKeyRef`. A config/secret change plus a rollout (as GitOps tools like Flux/ArgoCD already do) picks up the new config on next boot — no hot-reload, no CRD, no extra RBAC. 

 - Provider key (`google`, `keycloak`, ...) is the stable slug: used in redirect URL (`/auth/oidc/{id}/callback`) and in the discovery-endpoint response. Map key replaces the earlier `id:` list-item field. 
 - `issuer_url`: OIDC issuer identifier, not the discovery endpoint itself — backend fetches `{issuer_url}/.well-known/openid-configuration` at startup to resolve authorize/token/jwks endpoints (standard OIDC discovery — this is the "OpenID Provider Metadata" document, distinct from OAuth Dynamic Client Registration metadata, which this design deliberately does not use since providers are statically pre-registered by an admin). discovery). 
 - `icon`: slug resolved against homarr-labs/dashboard-icons (https://github.com/homarr-labs/dashboard-icons), served via their CDN. `icon_url`: explicit override for custom/self-hosted providers not in that set. 

 Non-k8s/simple deployments: same placeholder env-var override mechanism still works standalone (set whatever env var each `${...}` reference names), `OIDC_PROVIDERS__<ID>__*` directly), config file path still required via e.g. `OIDC_CONFIG_FILE`. If `OIDC_CONFIG_FILE` is unset, OIDC is disabled entirely (empty provider registry, no boot failure). 

 **Startup discovery is fail-fast**: if any configured provider's `.well-known/openid-configuration` fetch fails at boot, the whole server aborts (matches existing `DATABASE_URL`/`APP_SECRET` "bad config = don't start" behavior) — no partial-availability state to reason about. 

 **Layering**: Domain gets a `ProviderKey`/`IssuerUrl`/`ClientId`/`ClientSecret`/`OidcProviderConfig` value-object set plus a port pair — `OidcProviderRegistry` (lookup discovered client + config by `ProviderKey`) and a minimal marker-shaped `OidcClient` trait (just proves a discovered client is reachable through the port; #256 extends it with the actual authorize/token-exchange methods once that ticket designs the flow). Infra implements both via `openidconnect-rs` (discovery, `CoreClient`) — no `openidconnect-rs`/`serde_yaml` dependency leaks into Domain or Application. Config loading itself has no Application-layer use case (it's boot-time wiring, same category as `Core::build()`, not a request-time Command/Query).

Back