This repository has no description
0

Configure Feed

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

modules.zitadel: support generic OIDC IdPs

idps.<name>.type = "oidc" with an issuer, a literal clientId as
alternative to clientIdFile, a display name independent of the attribute
name and per-IdP providerOptions (default unchanged: linking, creation and
auto update allowed). Generic OIDC IdPs use PKCE and ID token mapping.
Zitadel 2.71 does not fetch the issuer's discovery document when the IdP
is created, only at login, so the reconciler needs no ordering on the
provider.

+85 -29
+67 -19
modules/zitadel/default.nix
··· 45 45 idps = lib.mkOption { 46 46 default = { }; 47 47 description = '' 48 - External identity providers, by display name, added to the login 49 - policy. Users may log in, register and link accounts with them. An 50 - IdP whose credential files are empty is skipped. 48 + External identity providers, added to the login policy. An IdP whose 49 + client id or secret is empty is skipped. 51 50 ''; 52 51 type = lib.types.attrsOf ( 53 - lib.types.submodule { 54 - options = { 55 - type = lib.mkOption { 56 - type = lib.types.enum [ 57 - "github" 58 - "google" 59 - ]; 52 + lib.types.submodule ( 53 + { name, ... }: 54 + { 55 + options = { 56 + type = lib.mkOption { 57 + type = lib.types.enum [ 58 + "github" 59 + "google" 60 + "oidc" 61 + ]; 62 + }; 63 + name = lib.mkOption { 64 + type = lib.types.str; 65 + default = name; 66 + description = "Display name on the login page; the IdP is found by it."; 67 + }; 68 + issuer = lib.mkOption { 69 + type = lib.types.nullOr lib.types.str; 70 + default = null; 71 + description = "Issuer of a generic OIDC IdP (type = \"oidc\")."; 72 + }; 73 + clientId = lib.mkOption { 74 + type = lib.types.nullOr lib.types.str; 75 + default = null; 76 + }; 77 + clientIdFile = lib.mkOption { 78 + type = lib.types.nullOr lib.types.str; 79 + default = null; 80 + }; 81 + clientSecretFile = lib.mkOption { type = lib.types.str; }; 82 + scopes = lib.mkOption { 83 + type = lib.types.listOf lib.types.str; 84 + default = [ 85 + "openid" 86 + "profile" 87 + "email" 88 + ]; 89 + }; 90 + providerOptions = lib.mkOption { 91 + type = lib.types.attrsOf lib.types.bool; 92 + default = { 93 + isLinkingAllowed = true; 94 + isCreationAllowed = true; 95 + isAutoUpdate = true; 96 + }; 97 + description = '' 98 + Options of the IdP, as in Zitadel's admin API 99 + (zitadel.idp.v1.Options). 100 + ''; 101 + }; 60 102 }; 61 - clientIdFile = lib.mkOption { type = lib.types.str; }; 62 - clientSecretFile = lib.mkOption { type = lib.types.str; }; 63 - scopes = lib.mkOption { type = lib.types.listOf lib.types.str; }; 64 - }; 65 - } 103 + } 104 + ) 66 105 ); 67 106 }; 68 107 ··· 154 193 }; 155 194 156 195 config = lib.mkIf zitadel.enable { 157 - assertions = lib.mapAttrsToList (name: project: { 158 - assertion = project.users == [ ] || project.access == "declarative"; 159 - message = "lass.zitadel.projects.${name}.users needs access = \"declarative\""; 160 - }) cfg.projects; 196 + assertions = 197 + lib.mapAttrsToList (name: project: { 198 + assertion = project.users == [ ] || project.access == "declarative"; 199 + message = "lass.zitadel.projects.${name}.users needs access = \"declarative\""; 200 + }) cfg.projects 201 + ++ lib.mapAttrsToList (name: idp: { 202 + assertion = (idp.clientId == null) != (idp.clientIdFile == null); 203 + message = "lass.zitadel.idps.${name} needs exactly one of clientId and clientIdFile"; 204 + }) cfg.idps 205 + ++ lib.mapAttrsToList (name: idp: { 206 + assertion = (idp.type == "oidc") == (idp.issuer != null); 207 + message = "lass.zitadel.idps.${name}.issuer is needed for, and only for, type = \"oidc\""; 208 + }) cfg.idps; 161 209 162 210 services.zitadel.steps.FirstInstance = { 163 211 PatPath = patFile;
+18 -10
modules/zitadel/reconcile.py
··· 88 88 linked = { 89 89 i["idpId"] for i in search("/admin/v1/policies/login/idps/_search") 90 90 } 91 - for name, idp in config["idps"].items(): 92 - with open(idp["clientIdFile"]) as f: 93 - client_id = f.read().strip() 91 + IDP_PATHS = {"github": "github", "google": "google", "oidc": "generic_oidc"} 92 + for idp in config["idps"].values(): 93 + name = idp["name"] 94 + client_id = idp["clientId"] 95 + if client_id is None: 96 + with open(idp["clientIdFile"]) as f: 97 + client_id = f.read().strip() 94 98 with open(idp["clientSecretFile"]) as f: 95 99 client_secret = f.read().strip() 96 100 if not client_id or not client_secret: ··· 101 105 "clientId": client_id, 102 106 "clientSecret": client_secret, 103 107 "scopes": idp["scopes"], 104 - "providerOptions": { 105 - "isLinkingAllowed": True, 106 - "isCreationAllowed": True, 107 - "isAutoUpdate": True, 108 - }, 108 + "providerOptions": idp["providerOptions"], 109 109 } 110 + if idp["type"] == "oidc": 111 + body |= { 112 + "issuer": idp["issuer"], 113 + # user info from the ID token, without a userinfo request 114 + "isIdTokenMapping": True, 115 + "usePkce": True, 116 + } 117 + path = f"/admin/v1/idps/{IDP_PATHS[idp['type']]}" 110 118 existing = by_name(providers, name) 111 119 if existing: 112 120 idp_id = existing["id"] 113 - call("PUT", f"/admin/v1/idps/{idp['type']}/{idp_id}", body) 121 + call("PUT", f"{path}/{idp_id}", body) 114 122 else: 115 - idp_id = call("POST", f"/admin/v1/idps/{idp['type']}", body)["id"] 123 + idp_id = call("POST", path, body)["id"] 116 124 print(f"idp {name}: created") 117 125 if idp_id not in linked: 118 126 call("POST", "/admin/v1/policies/login/idps", {"idpId": idp_id})