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);
});
})();