This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

configs.hass: log in via id.lassul.us (auth_oidc)

Zitadel project hass on neoprism with a public app (PKCE, no secret) and access = "managed": users get access via a user grant an admin gives in the id.lassul.us console. On acheron the auth_oidc custom component from nixpkgs is mounted into the Home Assistant container and configured via /config/packages, next to the username/password login.

modules.zitadel gets authMethod = "none" for public clients: reconcile generates no secret for them and writes only the client id to the env file, so nothing secret has to reach acheron.

Zitadel generates the client id, so it cannot be known at eval time on acheron. It is not secret; after neoprism is deployed it is copied from /var/lib/zitadel-apps/hass-web.env into the prompted hass-oidc var and passed to the container as HASS_OIDC_CLIENT_ID (!env_var in the yaml).

automatic_user_linking reuses the existing HA user whose username equals the preferred_username claim instead of creating a new one; it stays on because only users granted the project can log in. The groups scope is not requested since Zitadel has no groups claim.

+61 -9
+1
configs/hass/default.nix
··· 3 3 imports = [ 4 4 ./zigbee.nix 5 5 ./monitoring.nix 6 + ./oidc.nix 6 7 ]; 7 8 8 9 # Runs on the gg23 router: the LAN (int0) is a trusted interface there
+32
configs/hass/oidc.nix
··· 1 + # Login via id.lassul.us (Zitadel project hass, configs/hass/proxy.nix) with 2 + # the auth_oidc custom component. The username/password login stays. 3 + { config, pkgs, ... }: 4 + { 5 + # Loaded from /config/packages like configs/hass/monitoring.nix. 6 + # automatic_user_linking: the first login reuses the HA user whose username 7 + # equals the preferred_username claim; only users granted the project can 8 + # log in at all. 9 + virtualisation.oci-containers.containers.homeassistant = { 10 + volumes = [ 11 + "${pkgs.home-assistant-custom-components.auth_oidc}/custom_components/auth_oidc:/config/custom_components/auth_oidc:ro" 12 + "${pkgs.writeText "auth_oidc.yaml" '' 13 + auth_oidc: 14 + client_id: !env_var HASS_OIDC_CLIENT_ID 15 + discovery_url: https://id.lassul.us/.well-known/openid-configuration 16 + display_name: lassul.us ID 17 + features: 18 + automatic_user_linking: true 19 + include_groups_scope: false 20 + ''}:/config/packages/auth_oidc.yaml:ro" 21 + ]; 22 + environmentFiles = [ config.clan.core.vars.generators.hass-oidc.files.env.path ]; 23 + }; 24 + 25 + clan.core.vars.generators.hass-oidc = { 26 + files.env = { }; 27 + prompts.client-id.description = "Client id of the Zitadel app hass/web (CLIENT_ID in /var/lib/zitadel-apps/hass-web.env on neoprism)"; 28 + script = '' 29 + printf 'HASS_OIDC_CLIENT_ID=%s\n' "$(cat "$prompts"/client-id)" > "$out/env" 30 + ''; 31 + }; 32 + }
+12
configs/hass/proxy.nix
··· 8 8 proxyPass = "http://acheron.r:8123"; 9 9 }; 10 10 }; 11 + 12 + # Login via id.lassul.us (auth_oidc, configs/hass/oidc.nix), only for users 13 + # an admin gave a user grant on the project in the console. A public client 14 + # with PKCE; its client id is in the envFile and goes into the hass-oidc var 15 + # of acheron. 16 + lass.zitadel.projects.hass = { 17 + access = "managed"; 18 + apps.web = { 19 + redirectUris = [ "https://hass.lassul.us/auth/oidc/callback" ]; 20 + authMethod = "none"; 21 + }; 22 + }; 11 23 }
+7 -2
modules/zitadel/default.nix
··· 183 183 type = lib.types.enum [ 184 184 "basic" 185 185 "post" 186 + "none" 186 187 ]; 187 188 default = "basic"; 188 - description = "How the app sends its client secret to the token endpoint."; 189 + description = '' 190 + How the app sends its client secret to the token 191 + endpoint; "none" for a public client without a 192 + secret, which has to use PKCE. 193 + ''; 189 194 }; 190 195 refreshTokens = lib.mkOption { 191 196 type = lib.types.bool; ··· 200 205 path = lib.mkOption { 201 206 type = lib.types.str; 202 207 default = "${appsDir}/${project}-${name}.env"; 203 - description = "Env file with the app's client id and secret."; 208 + description = "Env file with the app's client id and secret (if any)."; 204 209 }; 205 210 owner = lib.mkOption { 206 211 type = lib.types.str;
+9 -7
modules/zitadel/reconcile.py
··· 232 232 new = {"name": app_name, **oidc_config} 233 233 res = call("POST", f"{base}/oidc", new) 234 234 app_id, client_id = res["appId"], res["clientId"] 235 - secret = res["clientSecret"] 235 + secret = res.get("clientSecret") 236 236 print(f"app {project_name}/{app_name}: created") 237 237 else: 238 238 app_id = existing["id"] ··· 246 246 if saved["clientId"] == client_id: 247 247 secret = saved["clientSecret"] 248 248 # Zitadel shows a secret only when it creates it, so without a saved 249 - # copy for this client the app gets a new one. 250 - if secret is None: 249 + # copy for this client the app gets a new one. Public clients have 250 + # none. 251 + public = app["authMethodType"] == "OIDC_AUTH_METHOD_TYPE_NONE" 252 + if secret is None and not public: 251 253 path = f"{base}/{app_id}/oidc_config/_generate_client_secret" 252 254 secret = call("POST", path, {})["clientSecret"] 253 255 print(f"app {project_name}/{app_name}: new client secret") ··· 257 259 })) 258 260 env = app["envFile"] 259 261 uid = pwd.getpwnam(env["owner"]).pw_uid 260 - write_file(env["path"], uid, ( 261 - f"{env['clientIdVariable']}={client_id}\n" 262 - f"{env['clientSecretVariable']}={secret}\n" 263 - )) 262 + content = f"{env['clientIdVariable']}={client_id}\n" 263 + if not public: 264 + content += f"{env['clientSecretVariable']}={secret}\n" 265 + write_file(env["path"], uid, content) 264 266 265 267 if project["access"] != "declarative": 266 268 continue