{ lib, config, ... }: let inherit (lib) mkIf mkOption mkMerge attrNames mapAttrsToList ; inherit (lib.types) str nullOr submodule listOf attrsOf ; cfg = config.xyno.services.oauth2Proxy; kanidmCfg = config.xyno.services.kanidm; oauth2ProxyInternalHostPort = "127.0.0.4:4180"; oauth2ProxyInternalHostPortMetrics = "127.0.0.4:4181"; in { options.xyno.services.oauth2Proxy = { domain = mkOption { default = "oauth.xyno.systems"; type = str; }; hosts = mkOption { type = attrsOf ( submodule ( { name, ... }: { options.allowedGroups = mkOption { type = nullOr (listOf str); default = null; }; options.allowed_email_domains = mkOption { type = nullOr (listOf str); default = null; }; options.allowed_emails = mkOption { type = nullOr (listOf str); default = null; }; options.middlewares = mkOption { type = listOf str; description = "add to your service"; default = [ "oauth-errors" "oauth-host-${name}" ]; }; } ) ); example = { "navidrome.xyno.systems" = { allowedGroups = [ "navidrome_access@idm.xyno.systems" ]; }; }; default = { }; }; }; config = mkIf (cfg.enable && config.xyno.services.kanidm.enable) { services.kanidm.provision = { groups = { proxy_users.members = [ "application_admins" ]; }; systems.oauth2.oauth2_proxy = { displayName = "oauth2 proxy"; originUrl = [ "https://${cfg.domain}/oauth2/callback" ] ++ (mapAttrsToList (n: v: "https://${n}/oauth2/callback") cfg.hosts); originLanding = "https://${cfg.domain}/oauth2/sign_in"; preferShortUsername = true; claimMaps = { "proxy_group" = { joinType = "array"; valuesByGroup = { "proxy_users" = [ "proxy_users" ]; }; }; }; scopeMaps."proxy_users" = [ "email" "openid" ]; }; }; xyno.services.kanidm.templates.oauth2_proxy = { wantedBy = [ "oauth2-proxy.service" ]; text = p: '' OAUTH2_PROXY_CLIENT_ID=${p.clientId} OAUTH2_PROXY_CLIENT_SECRET=${p.basicSecret} OAUTH2_PROXY_COOKIE_SECRET=${p.env "COOKIE_SECRET"} OAUTH2_PROXY_OIDC_ISSUER_URL=https://${kanidmCfg.domain}/oauth2/openid/${p.clientId} ''; environmentFiles = [ config.sops.templates.oauth2ProxyEnv.path ]; }; sops.secrets."oauth2Proxy/cookieSecret" = { sopsFile = ../../../instances/${config.networking.hostName}/secrets/kanidm.yaml; }; sops.templates."oauth2ProxyEnv" = { restartUnits = [ "generate-kanidm-template-oauth2_proxy.service" ]; content = '' COOKIE_SECRET=${config.sops.placeholder."oauth2Proxy/cookieSecret"} ''; }; xyno.services.monitoring.exporters.oauth2Proxy = "http://${oauth2ProxyInternalHostPortMetrics}"; systemd.services.oauth2Proxy.after = [ "traefik.service" ]; xyno.services.oauth2Proxy = { environmentFiles = [ kanidmCfg.templates.oauth2_proxy.path ]; settings = mkMerge [ { provider = "oidc"; scope = "openid email"; oidc_groups_claim = "proxy_group"; allowed_groups = [ "proxy_users" ]; http_address = "${oauth2ProxyInternalHostPort}"; https_address = ""; whitelist_domains = attrNames cfg.hosts; email_domains = "*"; skip_provider_button = true; code_challenge_method = "S256"; set_xauthrequest = true; } (mkIf config.xyno.services.monitoring.enable { metrics_address = "http://${oauth2ProxyInternalHostPortMetrics}"; }) ]; }; xyno.services.traefik.simpleProxy = mkMerge ( [ { oauth = { rule = "Host(`${cfg.domain}`) && PathPrefix(`/oauth2`)"; internal = "http://${oauth2ProxyInternalHostPort}"; middlewares = [ "auth-headers" ]; host = cfg.domain; }; } ] ++ (mapAttrsToList (n: v: { "oauth-host-${n}" = { rule = "Host(`${n}`) && PathPrefix(`/oauth2`)"; internal = "http://${oauth2ProxyInternalHostPort}"; middlewares = [ "auth-headers" ]; host = n; }; }) cfg.hosts) ); services.traefik.dynamicConfigOptions.http.middlewares = mkMerge ( (mapAttrsToList (n: v: { "oauth-host-${n}" = let maybeQueryArg = name: value: if name == "middlewares" || value == null then null else "${name}=${lib.concatStringsSep "," (builtins.map lib.escapeURL value)}"; allArgs = lib.mapAttrsToList maybeQueryArg v; cleanArgs = builtins.filter (x: x != null) allArgs; cleanArgsStr = lib.concatStringsSep "&" cleanArgs; in { forwardAuth = { address = "https://${cfg.domain}/oauth2/auth?${cleanArgsStr}"; authResponseHeaders = [ "X-Auth-Request-User" "X-Auth-Request-Groups" "X-Auth-Request-Email" "X-Auth-Request-Preferred-Username" ]; trustForwardHeader = true; }; }; }) cfg.hosts) ++ [ { auth-headers.headers = { frameDeny = true; contentTypeNosniff = true; }; oauth-errors.errors = { status = [ "401-403" ]; service = config.xyno.services.traefik.simpleProxy.oauth.serviceName; query = "/oauth2/sign_in?rd={url}"; }; } ] ); }; }