Add Authentik SSO integration for Jellyfin (#239)

## Summary
- Add Authentik OIDC provider + application for Jellyfin via blueprint (all authenticated users allowed, no policy binding)
- Wire `jellyfin-client-secret` through ExternalSecret and Authentik worker deployment
- Install [jellyfin-plugin-sso](https://github.com/9p4/jellyfin-plugin-sso) v4.0.0.3 via Ansible, with OIDC config template
- Authentik `admins` group maps to Jellyfin administrator role
- Local login left enabled; SSO is additive

## Deployment and Testing
- [ ] Sync ArgoCD `authentik` app on branch — verify provider + application appear in Authentik admin
- [ ] `mise run provision-indri -- --tags jellyfin --check --diff` (dry run)
- [ ] `mise run provision-indri -- --tags jellyfin` (deploy plugin + config)
- [ ] Test SSO flow: `https://jellyfin.ops.eblu.me/sso/OID/start/authentik`
- [ ] Verify `eblume` account auto-links via `preferred_username` match
- [ ] Verify admins group → Jellyfin admin
- [ ] Reset ArgoCD app revision to main after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/239
This commit is contained in:
Erich Blume 2026-02-21 20:05:44 -08:00
commit 07fb48626d
8 changed files with 142 additions and 0 deletions

View file

@ -161,6 +161,23 @@
no_log: true no_log: true
tags: [caddy] tags: [caddy]
# Jellyfin SSO client secret
- name: Fetch Jellyfin OIDC client secret
ansible.builtin.command:
cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/oor7os5kapczgpbwv7obkca4y4/jellyfin-client-secret"
delegate_to: localhost
register: _jellyfin_oidc_secret
changed_when: false
no_log: true
check_mode: false
tags: [jellyfin]
- name: Set Jellyfin OIDC client secret fact
ansible.builtin.set_fact:
jellyfin_sso_client_secret: "{{ _jellyfin_oidc_secret.stdout }}"
no_log: true
tags: [jellyfin]
# Jellyfin API key for metrics collection # Jellyfin API key for metrics collection
- name: Fetch Jellyfin API key - name: Fetch Jellyfin API key
ansible.builtin.command: ansible.builtin.command:

View file

@ -21,3 +21,10 @@ jellyfin_webdir: "{{ jellyfin_cask_app_path }}/Contents/Resources/jellyfin-web"
# Log directory # Log directory
jellyfin_log_dir: "{{ ansible_env.HOME }}/Library/Logs" jellyfin_log_dir: "{{ ansible_env.HOME }}/Library/Logs"
# SSO plugin configuration
jellyfin_sso_plugin_version: "4.0.0.3"
jellyfin_sso_client_id: jellyfin
jellyfin_sso_client_secret: ""
jellyfin_sso_provider_name: authentik
jellyfin_plugins_dir: "{{ jellyfin_data_dir }}/plugins"

View file

@ -28,3 +28,36 @@
when: jellyfin_launchctl_check.rc != 0 when: jellyfin_launchctl_check.rc != 0
changed_when: true changed_when: true
failed_when: false failed_when: false
# SSO plugin installation
- name: Ensure SSO-Auth plugin directory exists
ansible.builtin.file:
path: "{{ jellyfin_plugins_dir }}/SSO-Auth_{{ jellyfin_sso_plugin_version }}"
state: directory
mode: '0755'
- name: Download SSO-Auth plugin archive
ansible.builtin.get_url:
url: "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v{{ jellyfin_sso_plugin_version }}/sso-authentication_{{ jellyfin_sso_plugin_version }}.zip"
dest: "/tmp/sso-authentication_{{ jellyfin_sso_plugin_version }}.zip"
mode: '0644'
- name: Extract SSO-Auth plugin
ansible.builtin.unarchive:
src: "/tmp/sso-authentication_{{ jellyfin_sso_plugin_version }}.zip"
dest: "{{ jellyfin_plugins_dir }}/SSO-Auth_{{ jellyfin_sso_plugin_version }}"
remote_src: true
notify: Reload jellyfin
- name: Ensure plugin configurations directory exists
ansible.builtin.file:
path: "{{ jellyfin_plugins_dir }}/configurations"
state: directory
mode: '0755'
- name: Deploy SSO-Auth plugin configuration
ansible.builtin.template:
src: sso-auth.xml.j2
dest: "{{ jellyfin_plugins_dir }}/configurations/SSO-Auth.xml"
mode: '0644'
notify: Reload jellyfin

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- {{ ansible_managed }} -->
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SamlConfigs />
<OidConfigs>
<item>
<key><string>{{ jellyfin_sso_provider_name }}</string></key>
<value>
<PluginConfiguration>
<OidEndpoint>https://authentik.ops.eblu.me/application/o/jellyfin</OidEndpoint>
<OidClientId>{{ jellyfin_sso_client_id }}</OidClientId>
<OidSecret>{{ jellyfin_sso_client_secret }}</OidSecret>
<Enabled>true</Enabled>
<EnableAuthorization>true</EnableAuthorization>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles><string>admins</string></AdminRoles>
<Roles />
<EnableFolderRoles>false</EnableFolderRoles>
<FolderRoleMappings />
<RoleClaim>groups</RoleClaim>
<OidScopes>
<string>openid</string>
<string>email</string>
<string>profile</string>
</OidScopes>
<SchemeOverride>https</SchemeOverride>
<CanonicalLinks />
</PluginConfiguration>
</value>
</item>
</OidConfigs>
</PluginConfiguration>

View file

@ -245,3 +245,45 @@ data:
enabled: true enabled: true
negate: false negate: false
timeout: 30 timeout: 30
jellyfin.yaml: |
version: 1
metadata:
name: BlumeOps Jellyfin SSO
labels:
blueprints.goauthentik.io/description: "Jellyfin OIDC provider and application"
entries:
# OAuth2 provider for Jellyfin
- model: authentik_providers_oauth2.oauth2provider
id: jellyfin-provider
identifiers:
name: Jellyfin
attrs:
name: Jellyfin
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
client_type: confidential
client_id: jellyfin
client_secret: !Env AUTHENTIK_JELLYFIN_CLIENT_SECRET
redirect_uris:
- matching_mode: strict
url: https://jellyfin.ops.eblu.me/sso/OID/redirect/authentik
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
sub_mode: hashed_user_id
include_claims_in_id_token: true
# Jellyfin application — all authenticated users allowed (no policy binding)
- model: authentik_core.application
id: jellyfin-app
identifiers:
slug: jellyfin
attrs:
name: Jellyfin
slug: jellyfin
provider: !KeyOf jellyfin-provider
meta_launch_url: https://jellyfin.ops.eblu.me
policy_engine_mode: all

View file

@ -68,6 +68,11 @@ spec:
secretKeyRef: secretKeyRef:
name: authentik-config name: authentik-config
key: zot-client-secret key: zot-client-secret
- name: AUTHENTIK_JELLYFIN_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: authentik-config
key: jellyfin-client-secret
volumeMounts: volumeMounts:
- name: blueprints - name: blueprints
mountPath: /blueprints/custom mountPath: /blueprints/custom

View file

@ -49,3 +49,7 @@ spec:
remoteRef: remoteRef:
key: "Authentik (blumeops)" key: "Authentik (blumeops)"
property: zot-client-secret property: zot-client-secret
- secretKey: jellyfin-client-secret
remoteRef:
key: "Authentik (blumeops)"
property: jellyfin-client-secret

View file

@ -0,0 +1 @@
Add Authentik SSO to Jellyfin with admin group mapping