Files
ericxliu-me/posts/jellyfin-sso-with-authentik/index.html
2026-01-08 18:14:12 +00:00

74 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html><html lang=en><head><title>Setting Up Jellyfin SSO with Authentik: Surviving the Beta · Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="I recently integrated Jellyfin with Authentik for Single Sign-On (SSO). While the plugin works, it is still very much in an early development phase. The logging is often sparse or cryptic, and the feedback loop can be frustrating. Here is a guide focused on the obscure errors you might encounter and the simple fixes that aren&rsquo;t immediately obvious.
The Setup
Link to heading
The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Setting Up Jellyfin SSO with Authentik: Surviving the Beta"><meta name=twitter:description content="I recently integrated Jellyfin with Authentik for Single Sign-On (SSO). While the plugin works, it is still very much in an early development phase. The logging is often sparse or cryptic, and the feedback loop can be frustrating. Here is a guide focused on the obscure errors you might encounter and the simple fixes that arent immediately obvious.
The Setup Link to heading The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent."><meta property="og:url" content="https://ericxliu.me/posts/jellyfin-sso-with-authentik/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Setting Up Jellyfin SSO with Authentik: Surviving the Beta"><meta property="og:description" content="I recently integrated Jellyfin with Authentik for Single Sign-On (SSO). While the plugin works, it is still very much in an early development phase. The logging is often sparse or cryptic, and the feedback loop can be frustrating. Here is a guide focused on the obscure errors you might encounter and the simple fixes that arent immediately obvious.
The Setup Link to heading The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent."><meta property="og:locale" content="en"><meta property="og:type" content="article"><meta property="article:section" content="posts"><meta property="article:published_time" content="2025-11-15T00:00:00+00:00"><meta property="article:modified_time" content="2025-12-28T21:21:42+00:00"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/posts/jellyfin-sso-with-authentik/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script><script type=application/ld+json>{"@context":"http://schema.org","@type":"BlogPosting","headline":"Setting Up Jellyfin SSO with Authentik: Surviving the Beta","genre":"Blog","wordcount":"516","url":"https:\/\/ericxliu.me\/posts\/jellyfin-sso-with-authentik\/","datePublished":"2025-11-15T00:00:00\u002b00:00","dateModified":"2025-12-28T21:21:42\u002b00:00","description":"\u003cp\u003eI recently integrated Jellyfin with Authentik for Single Sign-On (SSO). While the plugin works, it is still very much in an early development phase. The logging is often sparse or cryptic, and the feedback loop can be frustrating. Here is a guide focused on the obscure errors you might encounter and the simple fixes that aren\u0026rsquo;t immediately obvious.\u003c\/p\u003e\n\u003ch2 id=\u0022the-setup\u0022\u003e\n The Setup\n \u003ca class=\u0022heading-link\u0022 href=\u0022#the-setup\u0022\u003e\n \u003ci class=\u0022fa-solid fa-link\u0022 aria-hidden=\u0022true\u0022 title=\u0022Link to heading\u0022\u003e\u003c\/i\u003e\n \u003cspan class=\u0022sr-only\u0022\u003eLink to heading\u003c\/span\u003e\n \u003c\/a\u003e\n\u003c\/h2\u003e\n\u003cp\u003eThe configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent.\u003c\/p\u003e","author":{"@type":"Person","name":"Eric X. Liu"}}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container post"><article><header><div class=post-title><h1 class=title><a class=title-link href=https://ericxliu.me/posts/jellyfin-sso-with-authentik/>Setting Up Jellyfin SSO with Authentik: Surviving the Beta</a></h1></div><div class=post-meta><div class=date><span class=posted-on><i class="fa-solid fa-calendar" aria-hidden=true></i>
<time datetime=2025-11-15T00:00:00Z>November 15, 2025
</time></span><span class=reading-time><i class="fa-solid fa-clock" aria-hidden=true></i>
3-minute read</span></div></div></header><div class=post-content><p>I recently integrated Jellyfin with Authentik for Single Sign-On (SSO). While the plugin works, it is still very much in an early development phase. The logging is often sparse or cryptic, and the feedback loop can be frustrating. Here is a guide focused on the obscure errors you might encounter and the simple fixes that aren&rsquo;t immediately obvious.</p><h2 id=the-setup>The Setup
<a class=heading-link href=#the-setup><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent.</p><h3 id=1-authentik-terraform>1. Authentik (Terraform)
<a class=heading-link href=#1-authentik-terraform><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Let Authentik manage the secrets. Don&rsquo;t hardcode them.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-hcl data-lang=hcl><span style=display:flex><span><span style=color:#ff7b72>resource</span> <span style=color:#a5d6ff>&#34;authentik_provider_oauth2&#34; &#34;jellyfin&#34;</span> {
</span></span><span style=display:flex><span> name <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;Jellyfin&#34;</span>
</span></span><span style=display:flex><span> client_id <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;jellyfin-ericxliu-me&#34;</span><span style=color:#8b949e;font-style:italic>
</span></span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic> # client_secret omitted -&gt; auto-generated
</span></span></span><span style=display:flex><span> property_mappings <span style=color:#ff7b72;font-weight:700>=</span> [
</span></span><span style=display:flex><span> <span style=color:#ff7b72>authentik_scope_mapping</span>.<span style=color:#ff7b72>openid</span>.<span style=color:#ff7b72>id</span>,
</span></span><span style=display:flex><span> <span style=color:#ff7b72>authentik_scope_mapping</span>.<span style=color:#ff7b72>profile</span>.<span style=color:#ff7b72>id</span>,
</span></span><span style=display:flex><span> <span style=color:#ff7b72>authentik_scope_mapping</span>.<span style=color:#ff7b72>email</span>.<span style=color:#ff7b72>id</span>,
</span></span><span style=display:flex><span> <span style=color:#ff7b72>authentik_scope_mapping</span>.<span style=color:#ff7b72>groups</span>.<span style=color:#ff7b72>id</span>
</span></span><span style=display:flex><span> ]<span style=color:#8b949e;font-style:italic>
</span></span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic> # ...
</span></span></span><span style=display:flex><span>}
</span></span></code></pre></div><h3 id=2-jellyfin-plugin-bashcurl>2. Jellyfin Plugin (Bash/Curl)
<a class=heading-link href=#2-jellyfin-plugin-bashcurl><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># ... (retrieve secret from terraform) ...</span>
</span></span><span style=display:flex><span>curl -X POST <span style=color:#a5d6ff>&#34;https://jellyfin.ericxliu.me/SSO/OID/Add/authentik&#34;</span> ... -d <span style=color:#a5d6ff>&#39;{
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;OidClientId&#34;: &#34;jellyfin-ericxliu-me&#34;,
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;OidSecret&#34;: &#34;&#39;</span><span style=color:#a5d6ff>&#34;</span><span style=color:#a5d6ff>${</span><span style=color:#79c0ff>SECRET</span><span style=color:#a5d6ff>}</span><span style=color:#a5d6ff>&#34;</span><span style=color:#a5d6ff>&#39;&#34;,
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;OidScopes&#34;: [&#34;openid&#34;, &#34;profile&#34;, &#34;email&#34;, &#34;groups&#34;],
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;SchemeOverride&#34;: &#34;https&#34;,
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;RoleClaim&#34;: &#34;groups&#34;
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> }&#39;</span>
</span></span></code></pre></div><h2 id=obscure-errors--fixes>Obscure Errors & Fixes
<a class=heading-link href=#obscure-errors--fixes><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>Because the plugin is still maturing, it doesn&rsquo;t always handle configuration errors gracefully. Here are the two main &ldquo;cryptic&rdquo; failures I encountered.</p><h3 id=1-the-value-cannot-be-null-crash>1. The &ldquo;Value cannot be null&rdquo; Crash
<a class=heading-link href=#1-the-value-cannot-be-null-crash><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>:
You attempt to start the SSO flow and get a generic 500 error. The Jellyfin logs show a C# exception:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>System.ArgumentNullException: Value cannot be null. (Parameter &#39;source&#39;)
</span></span><span style=display:flex><span> at System.Linq.Enumerable.Prepend[TSource](IEnumerable`1 source, TSource element)
</span></span><span style=display:flex><span> at Jellyfin.Plugin.SSO.Api.SSOController.OidChallenge(...)
</span></span></code></pre></div><p><strong>The Reality</strong>:
This looks like deep internal failure, but it&rsquo;s actually a simple configuration miss. The plugin code attempts to prepend &ldquo;openid profile&rdquo; to your configured scopes without checking if your scopes array exists first.
<strong>The Fix</strong>:
You <strong>must</strong> explicitly provide <code>"OidScopes"</code> in your JSON configuration. It cannot be null or omitted.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;OidScopes&#34;</span><span style=color:#f85149>:</span> [<span style=color:#a5d6ff>&#34;openid&#34;</span>, <span style=color:#a5d6ff>&#34;profile&#34;</span>, <span style=color:#a5d6ff>&#34;email&#34;</span>, <span style=color:#a5d6ff>&#34;groups&#34;</span>]
</span></span></code></pre></div><h3 id=2-the-httphttps-mismatch-redirect-loop>2. The HTTP/HTTPS Mismatch (Redirect Loop)
<a class=heading-link href=#2-the-httphttps-mismatch-redirect-loop><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>:
Authentik rejects the authorization request with &ldquo;Redirect URI mismatch&rdquo;, or the browser enters a redirect loop.
<strong>The Reality</strong>:
Jellyfin often sits behind a reverse proxy (Ingress/Traefik) terminating TLS. Use <code>Browser Developer Tools</code> to inspect the network requests. You will likely see the <code>redirect_uri</code> parameter encoded as <code>http://jellyfin...</code> instead of <code>https://</code>. configuration.
<strong>The Fix</strong>:
Do not rely on header forwarding magic. Force the scheme in the plugin configuration:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;SchemeOverride&#34;</span><span style=color:#f85149>:</span> <span style=color:#a5d6ff>&#34;https&#34;</span>
</span></span></code></pre></div><h3 id=3-case-sensitivity-in-json>3. Case Sensitivity in JSON
<a class=heading-link href=#3-case-sensitivity-in-json><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>: Configuration seems to be ignored or fields remain empty after a POST.
<strong>The Reality</strong>: The plugin&rsquo;s API controller keys are Case Sensitive in some versions/contexts.
<strong>The Fix</strong>: Stick to PascalCase for the keys (<code>OidEndpoint</code>, <code>AdminRoles</code>) as seen in the C# DTOs, rather than camelCase (<code>oidEndpoint</code>), unless the specific version documentation explicitly states otherwise. When in doubt, checking the source code (<code>SSOController.cs</code>) is often faster than trusting the README.</p><h2 id=summary>Summary
<a class=heading-link href=#summary><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>When debugging Jellyfin SSO, don&rsquo;t trust the UI to tell you what&rsquo;s wrong.</p><ol><li><strong>Check the logs</strong> (<code>kubectl logs</code>) for C# stack traces.</li><li><strong>Sanitize your JSON</strong> inputs (arrays can&rsquo;t be null).</li><li><strong>Inspect the URL parameters</strong> in your browser to see what Redirect URI is actually being generated.</li></ol><h3 id=references>References
<a class=heading-link href=#references><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><ul><li>Jellyfin SSO Plugin Repository: <code>https://github.com/9p4/jellyfin-plugin-sso</code></li><li>Authentik Documentation: <code>https://goauthentik.io/docs/providers/oauth2/</code></li><li>Jellyfin API Documentation: <code>https://api.jellyfin.org/</code></li></ul></div><footer><div id=disqus_thread></div><script>window.disqus_config=function(){},function(){if(["localhost","127.0.0.1"].indexOf(window.location.hostname)!=-1){document.getElementById("disqus_thread").innerHTML="Disqus comments not available by default when the website is previewed locally.";return}var t=document,e=t.createElement("script");e.async=!0,e.src="//ericxliu-me.disqus.com/embed.js",e.setAttribute("data-timestamp",+new Date),(t.head||t.body).appendChild(e)}(),document.addEventListener("themeChanged",function(){document.readyState=="complete"&&DISQUS.reset({reload:!0,config:disqus_config})})</script></footer></article><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css integrity=sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0 crossorigin=anonymous><script defer src=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js integrity=sha384-PwRUT/YqbnEjkZO0zZxNqcxACrXe+j766U2amXcgMg5457rve2Y7I6ZJSm2A0mS4 crossorigin=anonymous></script><script defer src=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js integrity=sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05 crossorigin=anonymous onload='renderMathInElement(document.body,{delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}]})'></script></section></div><footer class=footer><section class=container>©
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/f7528b3">[f7528b3]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>