fafflix,cheeztv: declarative SSO + LDAP plugin auth config #424
Reference in New Issue
Block a user
Delete Branch "benvin/jellyfin-plugin-config"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Why
Phase-1 jellyfin auth: browser SSO via the 9p4 SSO plugin (Authentik OIDC, ak_groups claim) and phone/TV app-passwords via the LDAP plugin against the Authentik LDAP outpost. Config is declarative — restarts overwrite UI edits. Activates when the plugin-baked jellyfin-ha image lands (separate PR).
Reviewed against the
9p4/jellyfin-plugin-sso,jellyfin/jellyfin-plugin-ldapauth, andjellyfin/jellyfin(BrandingOptions) source, and cross-checked againstterraform-authentik#28(branchbenvin/jellyfin-sso, head9760c2e). CI is green (kubeconform/pre-commit/vector-test all pass on571c3c0). One field-name/schema check is XML-valid but functionally blocks all SSO logins — please fix before this is safe to merge alongsideterraform-authentik#28.Blocking:
RoleClaim+OidScopeswill never authorize anyone via SSOSSO-Auth.xmlsets<RoleClaim>groups</RoleClaim>and<OidScopes>openid, profile, email</OidScopes>in bothapps/base/fafflix/plugin-configmap.yamlandapps/base/cheeztv/plugin-configmap.yaml.apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml,apps/base/logging/logviewer/oauth2-proxy-configmap.yaml) reads the group-membership claim asak_groups, notgroups. That's not incidental —terraform-authentik'smodules/authentik/main.tf(authentik_property_mapping_provider_scope.groups_hierarchical,scope_name = "ak_groups") unconditionally attaches anak_groupsscope mapping to every OAuth2 provider the module creates, including thejellyfinclient interraform-authentik#28'sconfig/providers_oauth2/jellyfin.yaml(whose own comment says "the plugin matching the ak_groups claim against akP-jellyfin-admin / akP-jellyfin-user"). Authentik never emits a claim literally namedgroups.scope_name.OidScopeshere is[openid, profile, email]—ak_groupsis never requested, so the claim would be absent from the token regardless.9p4/jellyfin-plugin-sso'sSSOController.cs(OidCallback, ~L177-269):timedState.Validis only settruewhenconfig.Rolesis non-empty (it is here:[akP-jellyfin-user, akP-jellyfin-admin]) and a claim of typeconfig.RoleClaimcarries a matching value. With noak_groupsclaim ever present,Validstaysfalsefor every user, including admins — this isn't "nobody gets admin," it's "nobody can log in via SSO at all."Fix: add
<string>ak_groups</string>toOidScopesand change<RoleClaim>groups</RoleClaim>to<RoleClaim>ak_groups</RoleClaim>in both instances'SSO-Auth.xml.Verified correct (no action needed)
terraform-authentik#28registers only the/sso/OID/redirect/authentikstrict URIs (no/r/alias) for all three hosts. In the plugin,OidChallenge(SSOController.csL360-384) derivesnewPathfrom whether the inbound request path contains/start/(whenisLinkingis false), and only falls back to the persistedconfig.NewPathotherwise. Sincebranding.xml's login link is/sso/OID/start/authentik, every fresh login always hits the/start/route and constructsredirect_uri=.../sso/OID/redirect/authentik— this matches the strict registration exactly. Good.BaseConfigurationManager.GetConfigurationFilemaps config key →{ConfigurationDirectoryPath}/{key}.xml, andBrandingConfigurationFactory.Key = "branding", so/config/config/branding.xmlis the correct target (matches the existinginject-configinitContainer'sdatabase.xml/encoding.xmlconvention in the same directory).SSO-Auth.xml'sOidConfig,LDAP-Auth.xml'sPluginConfiguration, andbranding.xml'sBrandingOptionsmatches the upstream C# property names/casing exactly (checked field-by-field against9p4/jellyfin-plugin-ssoConfig/PluginConfiguration.cs,jellyfin-plugin-ldapauthConfig/PluginConfiguration.cs, andjellyfinMediaBrowser.Model/Branding/BrandingOptions.cs).branding.xmlis write-if-absent (admin edits survive), files chowned1000:1000matching the pod'sfsGroup: 1000/ containerrunAsUser: 1000, and secret values come only fromoptional: trueenv vars sourced from the VSO-synced Secret — never baked into the ConfigMap. Consistent with the existinginject-configinitContainer's (pre-existing) root-chown pattern.DC=ldap,DC=goauthentik,DC=iomatchesterraform-authentik#28'sproviders_ldap/jellyfin-ldap.yamlbase_dnexactly.Advisory (non-blocking)
SkipSslVerify=trueon the LDAPS (6636) endpoint: the plugin exposesLdapRootCaPath(confirmed injellyfin-plugin-ldapauth'sPluginConfiguration.cs). Worth switching to pinning the outpost's CA via that field (combined-certs pattern used elsewhere in the repo) rather than disabling verification outright, once the outpost cert material is known — the PR body already flags this as a follow-up.https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/: diverges from the majority in-repo convention (arrstack,logviewer,litellmall useidentity.unkin.net).DoNotValidateIssuerNameisfalse(strict), so if Authentik's discovery document actually emitsissasidentity.unkin.net(its configured Brand domain) rather than the.k8s.syd1.au.unkin.netalias, token validation will fail. The PR body already flags this as an assumption to verify at first login — agreed this needs a live check, not something resolvable from source alone.inject-plugin-configsedonly escapes&,|,\for sed-safety, not for XML validity. If a rotatedclient_secretorldap_bind_passwordever contains a literal<,>, or unescaped&, the renderedSSO-Auth.xml/LDAP-Auth.xmlbecomes malformed XML on next pod restart. Low likelihood (Authentik client secrets are typically URL-safe base64), but worth a defensive XML-escape (sedfor&→&,<→<,>→>) in addition to the existing sed-metachar escaping.Everything else (VSS wiring, kustomization registration, mirroring across both instances, no image bump) looks correct and atomic.
RoleClaim + requested scopes switched to ak_groups per review — the hierarchical claim this estate actually emits.