Custom Repositories

SSO

Used to add Authentik SSO using OIDC.

https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json

Intro Skipper

Scans media files for the intros to add a skip button.

https://intro-skipper.org/manifest.json

IAmParadox27’s Repository

mainly used for File Transformation in conjunction with Javascript Injector that injects jellyfin-web files without needing to mount them in docker. I use this combination to fix SSO in Android.

https://www.iamparadox.dev/jellyfin/plugins/manifest.json

JavaScript Injector

Used to inject javascript in to index.html to make modifications. I like this over Custom Javascript because it works with File Transformation, I can have multiple ‘dropdowns’ for multiple scripts to keep things organized, I can disable scripts on the fly, and you don’t have to restart the server after modifying any scripts.

https://github.com/n00bcodr/Jellyfin-JavaScript-Injector

SSO Plugin

While LDAP login is possible natively, this plugin adds the ability for OIDC login using methods like Authentik. I prefer OIDC over LDAP because it is easier for me to manage and easier to learn in my personal opinion.

Authelia setup and config: https://github.com/9p4/jellyfin-plugin-sso/blob/main/providers.md#authentik

Authentik Setup and config: https://github.com/9p4/jellyfin-plugin-sso/blob/main/providers.md#authentik

Keycloak setup and config: https://github.com/9p4/jellyfin-plugin-sso/blob/main/providers.md#keycloak-oidc

Custom Javascript

Since the SSO sign-on does not currently work on mobile devices (as of Aug 2025), I needed to add some custom javascript to fix it.

If you are running Jellyfin in a Docker image not as root (as you should be), you may need to add a mount to the web index file or you may run in to permissions issues.

Source: https://github.com/9p4/jellyfin-plugin-sso/issues/189#issuecomment-2934082689

Modified to add customization for different providers such as Authentik, Authelia, Keycloak, and Zitadel.

// Configuration - Update this URL to match your SSO endpoint
const SSO_AUTH_URL = 'https://your-jellyfin-domain.com/sso/OID/start/your-service';
 
// SSO provider customization. Available options are:
// generic, authentik, authelia, keycloak, zitadel
const PROVIDER = 'generic';
  
// Self-executing function that waits for the document body to be available
(function waitForBody() {
  // If document.body doesn't exist yet, retry in 100ms
  if (!document.body) {
    return setTimeout(waitForBody, 100);
  }
  
  /**
   * Determines if the current page is a login page by checking multiple indicators
   * @returns {boolean} True if this appears to be a login page
   */
  function isLoginPage() {
    const hash = location.hash.toLowerCase();
    const pathname = location.pathname.toLowerCase();
    // Check for URL patterns that typically indicate login pages
    const hasLoginUrl = (
      hash === '' ||
      hash === '#/' ||
      hash === '#/home' ||
      hash === '#/login' ||
      hash.startsWith('#/login') ||
      pathname.includes('/login')
    );
  
    // Check for DOM elements that indicate a login form is present
    const hasLoginElements = (
      document.querySelector('input[type="password"]') !== null ||
      document.querySelector('.loginPage') !== null ||
      document.querySelector('#txtUserName') !== null
    );
  
    return hasLoginUrl || hasLoginElements;
  }
  
  /**
   * Checks if the current page should be excluded from SSO button insertion
   * These are typically pages where users are already authenticated
   * @returns {boolean} True if this page should be excluded
   */
  function shouldExcludePage() {
    const hash = location.hash.toLowerCase();
    // List of page patterns where we don't want to show the SSO button
    const excludePatterns = [
      '#/dashboard',
      '#/home.html',
      '#/movies',
      '#/tv',
      '#/music',
      '#/livetv',
      '#/search',
      '#/settings',
      '#/wizardstart',
      '#/wizardfinish',
      '#/mypreferencesmenu',
      '#/userprofile'
    ];
  
    return excludePatterns.some(pattern => hash.startsWith(pattern));
  }
  
  /**
   * Initializes the OAuth device ID in localStorage if it doesn't exist
   * This is required for Jellyfin native apps to maintain device identification
   */
  function oAuthInitDeviceId() {
    // Only set device ID if it's not already set and we're in a native shell environment
    if (!localStorage.getItem('_deviceId2') && window.NativeShell?.AppHost?.deviceId) {
      localStorage.setItem('_deviceId2', window.NativeShell.AppHost.deviceId());
    }
  }
  
  /**
   * Creates and inserts the SSO login button into the login page
   * Only runs if we're on a valid login page and the button doesn't already exist
   */
  function insertSSOButton() {
    // Safety check: ensure we're on the right page before proceeding
    if (!isLoginPage() || shouldExcludePage()) return;
  
    // Try to find a suitable container for the SSO button
    const loginContainer = document.querySelector('.readOnlyContent') ||
                          document.querySelector('form')?.parentNode ||
                          document.querySelector('.loginPage') ||
                          document.querySelector('#loginPage');
  
    // Exit if no container found or button already exists
    if (!loginContainer || document.querySelector('#custom-sso-button')) return;
  
    switch (PROVIDER.toLowerCase()) {
	  case 'authentik':
        SSO_BUTTON_HTML = '<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg" width="21em"><span>Login with Authentik</span>';
        break;
 
      case 'authelia':
		SSO_BUTTON_HTML = '<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authelia-light.svg" width="21em"><span>Login with Authelia</span>';
        break;
 
      case 'keycloak':
        SSO_BUTTON_HTML = '<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/keycloak.svg" width="21em"><span>Login with Keycloak</span>';
        break;
 
	  case 'zitadel':
        SSO_BUTTON_HTML = '<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/zitadel.svg" width="21em"><span>Login with Zitadel</span>';
        break;
  
      default:
        SSO_BUTTON_HTML = '<span class="material-icons">shield</span><span>Login with SSO</span>';
    }
  
    // Skip insertion for Jellyfin Media Player (JMP) as it may have different auth handling
    const isJMP = navigator.userAgent.includes("JellyfinMediaPlayer");
    if (isJMP) return;
  
    // Create the SSO button element
    const button = document.createElement('button');
    button.id = 'custom-sso-button';
    button.className = 'raised block emby-button button-submit';
    // Style the button to match Jellyfin's design while being visually distinct
    button.style = 'display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px 20px; font-size: 16px; background-color: #3949ab; color: white; margin-top: 16px;';
    // Add icon and text content
    button.innerHTML = SSO_BUTTON_HTML;
    // Handle button click - prevent form submission and redirect to SSO
    button.onclick = function (e) {
      e.preventDefault();
      oAuthInitDeviceId(); // Ensure device ID is set before SSO redirect
      window.location.href = SSO_AUTH_URL;
    };
  
    // Add the button to the login container
    loginContainer.appendChild(button);
  }
  
  // Initial setup: Check if we should insert the SSO button when script first loads
  if (isLoginPage() && !shouldExcludePage()) {
    // Delay insertion slightly to ensure all page elements are fully loaded
    setTimeout(insertSSOButton, 500);
  }
  
  // Set up a MutationObserver to watch for dynamic page changes
  // This handles cases where Jellyfin loads content dynamically via JavaScript
  const observer = new MutationObserver(() => {
    if (isLoginPage() && !shouldExcludePage()) {
      // Check if login elements are ready and button hasn't been inserted yet
      const ready = document.querySelector('.readOnlyContent') ||
                   document.querySelector('form') ||
                   document.querySelector('.loginPage');
      if (ready && !document.querySelector('#custom-sso-button')) {
        insertSSOButton();
      }
    }
  });
  
  // Start observing changes to the entire document body and its children
  observer.observe(document.body, { childList: true, subtree: true });
  
  // Listen for hash changes (when navigating between pages in Jellyfin's SPA)
  window.addEventListener('hashchange', () => {
    // Small delay to allow page transition to complete
    setTimeout(() => {
      if (isLoginPage() && !shouldExcludePage()) {
        insertSSOButton();
      }
    }, 300);
  });
})();