[ { "guid": "94879a0c-da24-4eb1-aa06-f28b4b9333b1", "name": "Jellyfin Security", "description": "Comprehensive Jellyfin security: TOTP & email 2FA, passkeys, OIDC/SSO sign-in (Google, Microsoft, Apple, Authelia, Authentik, Keycloak, PocketID, Cloudflare Access, ...), brute-force IP banning, impossible-travel detection, per-user IP allowlist, device pairing, trusted browsers, and audit log.", "overview": "Comprehensive Jellyfin security: 2FA, passkeys, OIDC/SSO, brute-force protection, impossible-travel detection, IP allowlist, and audit logging.", "owner": "ZL154", "category": "General", "imageUrl": "https://raw.githubusercontent.com/ZL154/JellyfinSecurity/main/assets/logo.png", "versions": [ { "version": "2.5.20.0", "changelog": "v2.5.20 - Mobile authentication, OIDC onboarding, and dashboard reliability. FIXED (#64, #137, #138): Android/mobile login controls, stale cached web shells, external-browser OIDC hand-off, Base URL/proxy redirects, and the post-2FA return path. ADDED (#134): the 2FA and first-login password screens identify the account being used; onboarding can be cancelled safely and revokes the temporary server session. SECURITY (#133, #135, #136): exact username linking is opt-in and conflict-safe; password setup requires a short-lived, single-use proof bound to the user/provider and a live Jellyfin session; symbol requirements are supported and enforced. FIXED (#123, #130, #131): valid package metadata and lifecycle handling, sortable audit logs, reliable security posture/navigation on Jellyfin 10.11.11, localized desktop/mobile sidebar entry, and packaged artwork. FIXED (#102, #124): portable ES256 passkey registration and deduplicated native-session notifications behind changing proxy addresses. Recovery-code documentation now matches the 600,000-iteration PBKDF2-HMAC-SHA256 implementation. All 847 keys are complete in 8 languages. 344/344 tests pass. In-place upgrade from any 2.5.x with no schema or configuration migration. Sigstore-signed and SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.20/Jellyfin.Plugin.TwoFactorAuthv2.5.20.0.zip", "checksum": "90EEDCF9FBD45F9B9B17CF0BD422E44C", "timestamp": "2026-07-24T21:25:58Z" }, { "version": "2.5.19.0", "changelog": "v2.5.19 - Authentik-friendly OIDC + clearer discovery errors. ADDED (#119, vasmarfas): a per-provider 'Don't force re-authentication (omit prompt=login)' toggle. The plugin normally sends prompt=login on the step-up and account-link flows so the IdP always re-authenticates the user (a security measure that stops a hijacked IdP session being silently confirmed). Some IdPs mishandle it - notably Authentik, whose upstream bug goauthentik/authentik#18507 returns a bare 404 on the sign-in redirect. The new toggle omits prompt=login for that provider only; off by default, localized in all 8 languages. FIXED (#120, MysaaJava): a wrong OIDC Discovery URL (issuer/realm root instead of the .well-known/openid-configuration document) used to surface a bare KeyNotFoundException in the logs; it now reports the missing field with guidance and auto-retries with the correct suffix appended. In-place upgrade from any 2.5.x, no schema or config changes. 266/266 tests pass. Verified against Keycloak and a live Authentik instance. Sigstore-signed + SLSA attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.19/Jellyfin.Plugin.TwoFactorAuthv2.5.19.0.zip", "checksum": "DD7A333CEDE58DEE91F85CE78E89E337", "timestamp": "2026-07-13T17:19:06Z" }, { "version": "2.5.18.0", "changelog": "Recovery codes on the verify screen, an accurate security score, and a tidier SSO hint; in-place upgrade from any 2.5.x, no config changes. FIXED: recovery codes are now offered on the \"verify your identity\" 2FA screen - when an already-signed-in session is asked to confirm 2FA, a Recovery tab now appears (alongside Authenticator / Email) whenever the account has unused recovery codes, so a user who lost their authenticator can fall back mid-session; previously that tab only showed during an emergency lockout even though the full login portal always offered it. FIXED: the security-posture \"2FA coverage\" score divided by every stored 2FA record including leftovers from deleted accounts, capping the score (e.g. \"10 / 30 - enroll 8 users\" when every current user was already enrolled); it now counts live Jellyfin users only. FIXED: the OIDC redirect-URI hint on the Sign-in Methods tab rendered its raw markup as literal text; it now formats correctly in all eight languages via an opt-in HTML-translation path in the i18n loader. Screenshots of the dashboard, login, enrollment and audit log are now in the README and wiki. Supports Jellyfin 10.11.9 and newer. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.18/Jellyfin.Plugin.TwoFactorAuthv2.5.18.0.zip", "checksum": "870B1E20F94FBE02D7EDEA6A898DA9F5", "timestamp": "2026-07-03T03:46:22Z" }, { "version": "2.5.17.0", "changelog": "App-password follow-up fixes, self-hosted ntfy delivery, and Keycloak roles / verified-email OIDC linking; in-place upgrade from any 2.5.x, no config changes. FIXED: app-password sessions on native clients such as Symfonium no longer fail on the request right after a successful login (\"Can't retrieve Media Info / Version\", a 403 \"Two-factor authentication required\") - this happened when the client's device id on the follow-up request differed from the login request, so the session wasn't recognised as already-verified; the app-password login now marks its session as 2FA-satisfied regardless, and the one-shot is consumed only by its own session so it can't carry over to a later login (#107). Self-hosted ntfy/Gotify/webhooks on a private LAN address (e.g. ntfy.mydomain.com behind a reverse proxy resolving to 192.168.x.x) were silently dropped by the SSRF egress guard (\"resolves to private address ... refusing to dispatch\"); a new opt-in \"Allow notifications to private/LAN addresses\" toggle (Settings > Push Notifications, off by default) lets them deliver, while link-local / cloud-metadata addresses (169.254.x, fe80::) stay blocked either way; the ntfy publish URL is also now built as {server}/{topic} (the topic is a path segment, not a header) (#116). OIDC verified-email account linking now works when the IdP encodes email_verified as a boolean, which was previously read as unverified and skipped, causing \"no email account match\" or a duplicate account (#95). NEW: Keycloak realm/client roles now drive group rules and admin elevation - Keycloak nests roles under realm_access.roles and resource_access.{client}.roles rather than a flat groups/roles claim, so Allowed groups / Admin groups / role-to-library mapping matched nothing; these nested roles are now read from the id_token and /userinfo (request the built-in roles scope and enable \"Add to ID token\" on the realm-roles mapper) (#95). Translations added for the new setting across all 8 languages. Supports Jellyfin 10.11.9 and newer. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.17/Jellyfin.Plugin.TwoFactorAuthv2.5.17.0.zip", "checksum": "2052E1CBBB0D06BE8C6C86CE189CE77C", "timestamp": "2026-07-03T01:46:28Z" }, { "version": "2.5.16.0", "changelog": "App passwords on native clients, sturdier OIDC onboarding, and contributor features; in-place upgrade from any 2.5.x, no config changes. FIXED: app passwords now work with native and third-party clients (Symfonium, Seerr/Jellyseerr, the mobile apps) - a TOTP-only account stayed on Jellyfin's default authentication provider, so its app password was checked against the real password and rejected (\"invalid password\"/\"credentials not correct\"); creating an app password now routes the account through the plugin's provider so the check actually runs (#102, #107, #108) - recreate any existing app password after updating; the OIDC set-password onboarding page always shows the configured complexity rules instead of sometimes defaulting to 16, surfaces a clear error if the policy can't load (with an escape hatch so you can't get locked out), and can no longer be skipped with the browser Back button (#100); clearer recovery when an in-app Android OIDC sign-in code expires or is blocked by a reverse proxy such as Cloudflare (#64). NEW: a per-provider \"Additional allowed CIDRs\" SSRF egress allowlist for non-RFC1918/link-local IdPs such as the rootless Podman host-gateway, opt-in and surgical (#103, andrewdunndev); an admin \"require password setup\" action to re-arm SMTP-less local-password recovery on a user's next OIDC sign-in (#104, andrewdunndev); an optional login-page setting to place SSO/2FA/passkey links below the Quick Connect button, and the native Forgot Password link is hidden when there is no password field (#79, ZEROX7). Translations completed and aligned across all 8 languages. Supports Jellyfin 10.11.9 and newer. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.16/Jellyfin.Plugin.TwoFactorAuthv2.5.16.0.zip", "checksum": "034D673C5EE6FBCA28126F58F6A304B7", "timestamp": "2026-06-27T22:54:18Z" }, { "version": "2.5.15.0", "changelog": "Targeted fix for #95; in-place upgrade from any 2.5.x, no config changes. FIXED: the Setup page no longer breaks behind a reverse proxy that rewrites HTML. If your proxy injects a browser-title/branding script via an nginx sub_filter on the head-closing tag (common for rebranding \"Jellyfin\" in the browser tab), it was also matching the same tag where it appeared inside one of the Setup page's own inline-JS strings (the recovery-codes print template) - injecting a script-closing tag mid-script, which prematurely ended the page's script element, dumped the rest of the page out as a wall of raw text, and left sections like \"Linked Sign-In Methods\" stuck on \"Loading...\" so admins couldn't link a provider (#95, chrisbehectik). The print template now builds every structural tag in split pieces, so no head/title/style/script close tag ever appears as a literal substring for a proxy sub_filter (or another plugin's injection middleware) to latch onto. Supports Jellyfin 10.11.9 and newer. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.15/Jellyfin.Plugin.TwoFactorAuthv2.5.15.0.zip", "checksum": "905637A156FC453F89F4457E2D0651F5", "timestamp": "2026-06-23T18:14:00Z" }, { "version": "2.5.14.0", "changelog": "Fix-heavy release; in-place upgrade from any 2.5.x, every new option opt-in. FIXED: a pending-2FA state tracked per-browser could short-circuit every API call and make Jellyfin Web unreachable (\"server cannot be reached\") until a restart - the client now routes you to the 2FA portal instead of dead-ending, and the OIDC bridge clears a stale flag before loading the app (#99, #98); OIDC role->library access, IdP-group->administrator elevation, and template-user permission copy now actually persist - they were written via an API that does not save user-policy fields on Jellyfin 10.11.x and are now applied via UpdatePolicyAsync, the same path the dashboard uses (#65, #96, #93); the \"Two-Factor Authentication\" entry returns on non-English UIs - it was anchored to the English word \"Profile\" (#101); untranslated admin labels fall back to readable English instead of a raw key. NEW: rename a provider's callback/redirect URL in place to fix a typo'd slug without delete+re-add, migrating existing SSO links, with the callback URL shown plus a Copy button (#94); an optional per-provider toggle to force new OIDC users to set a local Jellyfin password on first sign-in, with a configurable complexity policy (#100). Also: longer admin link-from-Setup window + diagnostics (#95). New admin-UI strings translated across all 8 languages. Supports Jellyfin 10.11.9 and newer. In-place upgrade from any 2.5.x, no schema or data migration. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.14/Jellyfin.Plugin.TwoFactorAuthv2.5.14.0.zip", "checksum": "7C03FF8A9F735169902F3B7536473931", "timestamp": "2026-06-23T17:24:22Z" }, { "version": "2.5.13.0", "changelog": "OIDC/SSO release — every new option is opt-in, so a default install behaves exactly like 2.5.12. NEW: admins can finally link OIDC from the Setup page, via a proper authenticated popup flow that links by subject instead of the sign-in resolver that refuses implicit admin links (#95); an optional per-provider \"Elevate matching users to administrator\" toggle (off by default) maps an IdP group to Jellyfin admin on sign-in - grant-only and logged at WARN (#96); an optional template user for auto-created accounts copies a chosen user's permissions and library access instead of Jellyfin's broad defaults (#93); the provider switch is split into \"Enable SSO\" and \"Show built-in button on login page\", so a custom login button can use the SSO URL while the plugin's own button stays hidden (#97). FIXED: OIDC sign-in no longer silently bounces back to the login screen when the post-auth AuthenticateByName fails (commonly an auth proxy intercepting it) - it now shows the real error plus a hint instead of an endless loop (#98). New admin-UI strings are translated across all 8 languages. Supports Jellyfin 10.11.9 and newer. In-place upgrade from any 2.5.x, no schema or data migration. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.13/Jellyfin.Plugin.TwoFactorAuthv2.5.13.0.zip", "checksum": "17D5545C90714C199AC81B66F3F22DC8", "timestamp": "2026-06-21T16:16:32Z" }, { "version": "2.5.12.0", "changelog": "v2.5.12 - full UI localization, passwordless 2FA login, Admins-Only enforcement fix, hide built-in recovery, Android SSO fallback. The login page, the 2FA setup page, the admin UI, and the injected sidebar entry now all follow Jellyfin's display language (auto-detected and remembered across restarts) across all 8 languages, and the scripts are served on cache-immune paths so the translations actually reach users behind a CDN instead of being frozen by a *.js edge-cache rule (#79). FIXES: users whose password is disabled can now sign in through the 2FA login portal with a blank password, matching Jellyfin's standard login page - both the page and the server endpoint were over-requiring a password (#82, cpb34); with enforcement scope set to Admins Only, a non-admin without 2FA logging in through the standard portal no longer hangs on 'Server Unavailable' - the session fail-safe now checks administrator status and fails safe so an admin is never accidentally exempted (#81, cpb34); when an embedded app WebView trips Google's disallowed_useragent block, the in-app sign-in dialog now reliably routes the user to 'Copy sign-in link, open in Chrome' (#64, fx-xt). NEW: an optional sub-toggle under email recovery hides Jellyfin's built-in 'Forgot password?' link so users see only the plugin's flow (#80, ZEROX7); clearer diagnostics when IdP email auto-fill doesn't populate (points at a missing email scope or wrong claim name). Dependency hygiene: QuestPDF pinned to 2026.5.0 (a newer build produced a broken recovery-codes PDF), analyzer + test-only bumps merged, and Microsoft.IdentityModel auto-bumps blocked (8.19.x breaks OIDC token validation). Supports Jellyfin 10.11.9 and newer. In-place upgrade from any 2.5.x, no schema or data migration. 266/266 tests pass on .NET 9. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.12/Jellyfin.Plugin.TwoFactorAuthv2.5.12.0.zip", "checksum": "2F1CDA2D45D17FD66484D411563EB9D7", "timestamp": "2026-06-17T12:45:00Z" }, { "version": "2.5.11.0", "changelog": "v2.5.11 - OIDC email auto-fill, disable-password login, email password recovery. NEW: disable username+password sign-in (OIDC / Quick Connect only) with three independently-toggleable escape hatches - administrators may always use a password, LAN clients may always use a password (i.e. disable for remote users only), and an explicit exempt-CIDR list - so a failed IdP can't lock everyone out (#69); per-provider custom login button text + icon/logo (#69); self-service password recovery by email - a one-time, 30-minute, single-use reset link, SMTP-gated, rate-limited per IP and per identifier, with always-generic responses so it can't be used to discover which accounts exist (#71); per-provider configurable OIDC email claim for IdPs that use a non-standard claim such as internal_email (#70). FIXES: the IdP email now populates the Jellyfin user's email field on sign-in when it isn't already set - used for email OTP and shown in the Users tab, and a manually-entered email is never overwritten (#70); a stale email record left behind by a DELETED Jellyfin account no longer counts as a duplicate that wrongly tripped the ambiguous-email guard and blocked OIDC sign-in for the surviving user; failed OIDC sign-ins now show a clear, actionable reason on the login page (no matching account, ambiguous email, not in an allowed group, MFA required, expired session) instead of silently bouncing - sensitive token details stay server-side. Supports Jellyfin 10.11.9 and newer. In-place upgrade from any 2.5.x, no schema or data migration. 266/266 tests pass on .NET 9. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.11/Jellyfin.Plugin.TwoFactorAuthv2.5.11.0.zip", "checksum": "C2A1F259ABFBF568DB2AA474FB3F95A4", "timestamp": "2026-06-15T00:44:38Z" }, { "version": "2.5.10.0", "changelog": "v2.5.10 - OIDC role & picture sync, real account lockout, empty-password gate. NEW: role-based library access from IdP group/role claims (#65) - per-provider, with an admin-UI mapping editor; works with identity providers that send a groups/roles claim (Authentik, Authelia, Keycloak, and similar; Google does not send groups). Profile-picture sync copies the IdP avatar into Jellyfin on sign-in (#66) - per-provider, SSRF-guarded, content-type-restricted. Per-provider 'always show the account chooser' (prompt=select_account) so the IdP shows its account picker instead of silently reusing an existing session - useful on shared machines / multiple accounts. 'Block sign-in with an empty password' is now an admin-UI toggle (was config-file-only). FIXES: account lockout now counts failed PASSWORDS, not just 2FA codes (#55) - enforced before the password check, with a clear 'temporarily locked' message on the login page, and administrators are exempt by default so the lockout cannot be used to deny-of-service the admin account (the per-IP brute-force ban still protects them). Auto-created OIDC users could be signed into with an empty password on Jellyfin 10.11.9+ because a later internal save overwrote the freshly-hardened password hash (#68) - fixed, and the empty-password gate now applies to every sign-in (not just OIDC/passkey users), so the new toggle also protects pre-existing passwordless accounts. OIDC identity matching is stricter: a sign-in is refused when the IdP email matches more than one Jellyfin user, and an administrator can never be resolved or auto-linked via IdP-asserted email (admins link explicitly from Setup). Also filled missing translations across all 8 locales. Supports Jellyfin 10.11.9 and newer. In-place upgrade from any 2.5.x, no schema or data migration. 263/263 tests pass on .NET 9. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.10/Jellyfin.Plugin.TwoFactorAuthv2.5.10.0.zip", "checksum": "9012E7CD0CBFF1324D5ADABCC1DA1646", "timestamp": "2026-06-14T14:06:53Z" }, { "version": "2.5.9.0", "changelog": "v2.5.9 - OIDC mobile sign-in + security hardening. Highlight: 'Sign in with Google' (and other OIDC/SSO providers) now works inside the Jellyfin mobile apps - the in-app browser was previously rejected by Google (403 disallowed_useragent) and dead-ended on the server screen; sign-in now opens in the device browser and returns you straight into the app, with per-provider 2FA-bypass preserved (issue #64). This release also includes a broad internal security-hardening pass across the authentication, OIDC/SSO, step-up, device-bypass and rate-limiting paths - no configuration changes or action needed on upgrade. Additionally: the injected login script is now served in a CDN/proxy-cache-safe way so an edge cache can no longer pin a stale copy after an upgrade; GeoIP database path handling is unified across lookups; and assorted reliability and resource-bounding improvements to in-memory sign-in state. In-place upgrade from any 2.5.x, no schema or data migration. 254/254 tests pass on .NET 9 / Jellyfin 10.11.x. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.9/Jellyfin.Plugin.TwoFactorAuthv2.5.9.0.zip", "checksum": "251808DA1CF00F7D705DBAB13AD0BA7F", "timestamp": "2026-06-13T23:22:49Z" }, { "version": "2.5.8.0", "changelog": "v2.5.8 - Admin step-up UX + smoke-test followups. FIXES: issue #57 (lorenatkin) admin Save Settings now triggers the existing step-up modal instead of silently rejecting when StepUpLevel is set - the main config POST was going through Jellyfin's built-in ApiClient.updatePluginConfiguration which doesn't recognise the 403 + stepUpRequired body, now flows through the same step-up-aware fetch wrapper every other gated admin endpoint uses, so the modal opens, accepts TOTP/recovery/passkey, verifies, and replays the original save transparently. StepUpLevel dropdown enum-name mismatch: Jellyfin's JsonStringEnumConverter serialises the enum by name (Off/Destructive/AllConfigChanges/Everything) but option values were 0/1/2/3 - dropdown went blank after save+refresh and parseInt('') silently posted 0, resetting the level to Off on every Save (which is also why subsequent settings toggles weren't gated). Option values now use enum names directly. Issue #55 followup (Dasnap): Users tab enumerates from Jellyfin's user table instead of plugin-side data files, so users who haven't yet interacted with the plugin show with all-zero counts instead of being invisible. Single-user read failures no longer 500 the whole listing. Issue #56 (cwildfoerster, derpacco): admin Hardening section gets a help block under Trusted Proxy CIDRs explaining the broad-RFC1918-range trap with concrete fix examples. SEC-H3 refusal log promoted from Debug to Information on first hit per peer IP (subsequent hits stay Debug to avoid spam) with the matched CIDR in the payload for self-diagnosis. Pending-pair Approve/Deny/QR buttons no longer fail silently - each click surfaces server errors via alert + console.error. Textarea clipping (LAN CIDRs, Trusted Proxy CIDRs, Admin emails, Exempt CIDRs, Restore JSON paste box) - .tfa-input now uses box-sizing:border-box so width:100% includes padding instead of bleeding outside parent panels. CODE-QUALITY: two CodeQL cs/cleartext-storage-of-sensitive-information false-positives dismissed (CIDRs are admin config, not credentials). UPGRADE: in-place, no schema breakage. If StepUpLevel was set non-Off on v2.5.7 the dropdown bug may have silently reset it - after upgrading + hard-refresh, re-pick the value and Save once. 254/254 tests pass on .NET 9 / Jellyfin 10.11.10. Sigstore-signed + SLSA build-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.8/Jellyfin.Plugin.TwoFactorAuthv2.5.8.0.zip", "checksum": "F0F6AB200853C2AB9C683598D3B45A17", "timestamp": "2026-06-07T17:00:00Z" }, { "version": "2.5.7.0", "changelog": "v2.5.7 - bug-fix and OIDC-experience release. Fixes issue #52 (sessions getting permanently 403'd by RequestBlockerMiddleware after docker compose down/up - verified-token state now persists as SHA-256 hashes in a sidecar so the in-memory set rehydrates on restart instead of locking everyone out), issue #54 (OIDC discovery rejected LAN-only / VPN-only IdPs - new per-provider AllowPrivateNetworks toggle, Advanced/default off, bypasses the v2.5.5 SSRF guard for THAT provider only), issue #55 (Users tab 500'd with Guid.Empty ArgumentException when a junk lockout entry existed - defensive skip in GetUsers plus write-refuse at the store boundary), and issue #50 followup (PR #53 by duongynhi000005-oss - login.html maxlength was still 6 so 8-digit email codes were truncated). Pending-pair Approve / Deny / QR buttons now show a spinner during the request and surface server errors via alert + console instead of silently dying. New: issue #48 feature (Gaarindor) - two independent admin toggles to hide the 'Sign in with 2FA' and 'Sign in with passkey' buttons inject.js adds to Jellyfin's main login page, so OIDC-only deployments look like OIDC-only deployments. New: OIDC step-up factor for users - users with only an IdP linked can satisfy SelfServiceStepUpMode=Forced by re-authenticating to that IdP in a popup, with subject-match against the stored SsoLink to prevent signing into a different account from granting step-up. In-place upgrade, no schema changes. 254/254 tests pass on .NET 9 / Jellyfin 10.11.10. Release artifacts cosign-signed and SLSA-provenance attested.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.7/Jellyfin.Plugin.TwoFactorAuthv2.5.7.0.zip", "checksum": "DCBCB2A39EFECD637856EC016E49E9B5", "timestamp": "2026-06-05T16:30:00Z" }, { "version": "2.5.6.0", "changelog": "v2.5.6 - Security and bug-fix release. Critical: closes a stolen-password account-takeover path where a known or guessed client-supplied DeviceId let an attacker skip 2FA via the registered-device / paired-device fallback (the signed trusted-device cookie path was always safe and is unchanged). New BareDeviceIdBypassEnabled config gates the legacy behaviour, default off. Also fixes issue #28 (OIDC redirect_uri built as http behind TLS-terminating reverse proxies - now defaults to https and infers the right scheme from the IdP discovery URL), issue #48 (OIDC sign-in failed when a pre-existing Jellyfin user matched the IdP preferred_username; now links instead of failing), issue #49 (login showed 'incorrect username or password' instead of the 2FA challenge - inject.js race + relative-URL matcher), and issue #50 (email OTP UI said 6-digit but server generated 8; UI now accepts 8 across all bundled languages). New: hardened-security policy for self-service 2FA changes (SelfServiceStepUpMode tri-state, default Forced) - adding or replacing any factor now requires the user to prove a current factor, and the proof can be a TOTP / recovery code OR a passkey assertion OR an emailed step-up code, picked from the prompt modal. Other UX: pending-pairings Deny button now sticks across the next auth retry, Setup page no longer reverts the passkey list to 'Loading' after the i18n pass runs, corrupt user-data JSON now fails closed instead of silently disabling 2FA, runtime CIDR parser rejects out-of-range prefixes, OIDC re-validates cached token/userinfo endpoints per use, ntfy and Gotify now go through the SSRF-pinned client, and passkey labels are sanitized on write and escaped on render. In-place upgrade, no schema changes, no required config edits. 254/254 tests pass on .NET 9 / Jellyfin 10.11.10.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.6/Jellyfin.Plugin.TwoFactorAuthv2.5.6.0.zip", "checksum": "891BA4A1B28EA7C6DC76F7F716EE2A95", "timestamp": "2026-06-04T19:00:00Z" }, { "version": "2.5.5.0", "changelog": "v2.5.5 - Security release. Fixes the OIDC empty-password sign-in path reported via Reddit by Throwaway-3586: newly auto-provisioned OIDC users were created without a password hash, which Jellyfin's default auth provider treats as accepting any password including empty. v2.5.5 sets a 256-bit random password on every OIDC-provisioned user at creation, so the empty-password sign-in is no longer accepted against new accounts. Also fixes the TOTP rotate endpoint (was broken in earlier versions, always rejected the current code) and bundles general security hardening across the auth pipeline (OIDC token algorithm allowlist, discovery-URL validation, recovery-code PBKDF2 bump, email-OTP storage hardening, GeoIP path validation, log scrubbing, race fixes). New optional config 'BlockEmptyPasswordLogin' (default off) extends the empty-password rejection to ALL users at the auth boundary - turn it on if your install doesn't need Jellyfin's kiosk-mode passwordless tile-click sign-in for family / guest accounts. In-place upgrade, no schema changes. One default-on behaviour change: the audit-log endpoint now requires a fresh step-up; lower StepUpLevel if that's inconvenient. 254/254 tests pass. Clean build with TreatWarningsAsErrors=true. Smoke-tested on a live 10.11.10 host: kiosk passwordless tile-click still works, admin 2FA + passkey + Google OIDC + Security Dashboard all functional.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.5/Jellyfin.Plugin.TwoFactorAuthv2.5.5.0.zip", "checksum": "9AD97255AD73D0699A29E3DC642E7FF3", "timestamp": "2026-06-04T01:00:00Z" }, { "version": "2.5.4.0", "changelog": "v2.5.4 - Bug fix release. Resolves issue #35 (Snerillinn) - Cloudflare Tunnel and reverse-proxy users were intermittently re-blocked with '[2FA] BLOCKED ... - 2FA not completed' several minutes after a successful passkey/2FA login, locking them out of the UI until a fresh re-auth. Root cause: AuthenticationEventHandler's four legitimate-bypass branches (IsDevicePreVerified, ConsumeQuickConnectPending, userDataPaired, BypassEvaluator.IsBypassed) called ChallengeStore.ApproveToken() to clear the 10-minute BlockToken TTL but did NOT call MarkTokenVerified() to set the 30-day verified flag. Subsequent SessionStarted events fire on every websocket reconnect, new browser tab, and idle-resume - and when those fire from a proxy IP that no longer matches the original bypass condition (very common behind Cloudflare Tunnel where the upstream IP can rotate), BypassEvaluator returns false on the second look, the handler walks the normal challenge path, and re-blocks the same token that was already legitimately approved minutes earlier. Fix: all four bypass branches now ALSO MarkTokenVerified(token), so the 30-day verified flag short-circuits any later SessionStarted re-evaluation regardless of which proxy IP the reconnect arrives from. Bonus: bumps Jellyfin.Controller + Jellyfin.Model SDK targets from 10.11.8 to 10.11.10, with reflection-based adaptation to the 10.11.10 IUserManager.Users (property) -> GetUsers() (method) rename - typeof(IUserManager).GetMethod('GetUsers') preferred at runtime with fallback to the legacy property via string-named GetProperty, so the same DLL still loads on 10.11.0-10.11.9 unchanged. Applied to TwoFactorAuthController.EnumerateAllUsers, StatsService.EnumerateUsers, and SecurityScoreService.EnumerateUsers. Also bumps MailKit 4.16.0 -> 4.17.0 (SMTP fixes upstream) and MaxMind.Db 5.0.0 -> 5.1.0 (GeoLite2 reader perf). 254/254 tests pass on the bumped SDK. Clean build with TreatWarningsAsErrors=true. In-place upgrade - no config changes, no schema changes, no breaking changes for any reverse-proxy or direct-access topology.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.4/Jellyfin.Plugin.TwoFactorAuthv2.5.4.0.zip", "checksum": "0301F57B83B7F8734BCFDCA3D82AD85D", "timestamp": "2026-06-02T21:30:00Z" }, { "version": "2.5.3.0", "changelog": "v2.5.3 - Follow-up to v2.5.2 (issue #37). Completes the ABI-compatibility fix begun in v2.5.2: the audit that should have run before v2.5.2 missed one more Jellyfin manager property access in the same dashboard endpoint - _sessionManager.Sessions at TwoFactorAuthController:3679 (Overview activeSessions KPI), :2342 (RevokeSession admin endpoint), :2796 (MySessions device enrichment). Same risk class as the _userManager.Users line patched in v2.5.2 - the property's return type changed between Jellyfin 10.11.8 and 10.11.9+, so the IL-emitted callvirt threw System.MissingMethodException at runtime on a 10.11.9+ host. In practice this meant v2.5.2's dashboard fix was incomplete: a 10.11.10 server would throw on the Users line BEFORE v2.5.2 and would throw on the Sessions line AFTER v2.5.2 - the dashboard still returned 'Failed to load dashboard' from a different line. v2.5.3 actually completes the fix. New TwoFactorAuthController.EnumerateSessions() private helper mirrors the existing EnumerateAllUsers() pattern - typeof(ISessionManager).GetProperty(nameof(Sessions)).GetValue() re-binds at runtime against whatever IEnumerable shape the running Jellyfin actually exposes, items downcast to SessionInfo, with try/catch + empty-yield fallback so endpoints degrade gracefully instead of 500-ing. All three callsites routed through it. Full audit of every injected-manager surface (IUserManager, ISessionManager, IDeviceManager, IServerApplicationHost, etc.) verified zero risky direct property accesses remain after v2.5.3 - all other manager calls are method-based with stable signatures across 10.11.x. Plugin now supports Jellyfin 10.11.0 through 10.11.10+ for every code path that depends on injected manager interfaces, and is structurally resistant to future return-type refactors in 10.11.11+ because reflection bypasses static IL binding. 254/254 tests pass. Clean build with TreatWarningsAsErrors=true. In-place upgrade.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.3/Jellyfin.Plugin.TwoFactorAuthv2.5.3.0.zip", "checksum": "3FE58EE02C192911391496763EDD73E1", "timestamp": "2026-05-31T01:20:00Z" }, { "version": "2.5.2.0", "changelog": "v2.5.2 - Bug fix release. Restores the admin Security Dashboard on Jellyfin 10.11.9+ servers. Issue #37: dashboard endpoint and the security-score 'all admins protected' factor both walked _userManager.Users via a statically-bound IL call. Between Jellyfin 10.11.8 (the ABI compiled against) and 10.11.9 the IUserManager.Users return type changed, so the plugin threw System.MissingMethodException at runtime on the newer ABI - surfaced to admins as 'Failed to load dashboard'. Fix routes both broken callsites through the same reflection-based shim already in production at TwoFactorAuthController.EnumerateAllUsers and StatsService.EnumerateUsers - Type.GetProperty(nameof(IUserManager.Users)).GetValue(_userManager) re-binds at runtime against whatever IEnumerable shape the running Jellyfin actually exposes, so it works across 10.11.8 / 10.11.9 / 10.11.10 and any future return-type change that still implements IEnumerable. Patched: TwoFactorAuthController.GetDashboardOverview enrollment-by-role loop, SecurityScoreService.AreAllAdminsProtected (security-score factor #2). Added private EnumerateUsers() helper to SecurityScoreService mirroring the existing pattern. No config / enrollment / OIDC / audit-log changes. 254/254 tests pass. Clean build with TreatWarningsAsErrors=true. In-place upgrade - Jellyfin 10.11.8 users see no behavioural difference, 10.11.9+ users get a working dashboard again.", "targetAbi": "10.11.0.0", "sourceUrl": "https://github.com/ZL154/JellyfinSecurity/releases/download/v2.5.2/Jellyfin.Plugin.TwoFactorAuthv2.5.2.0.zip", "checksum": "D8FCEE9BDA3AF029F6152C6AA16F08D6", "timestamp": "2026-05-31T01:00:00Z" }, { "version": "2.5.1.0", "changelog": "v2.5.1 - OIDC enhancements + i18n bugfixes + admin UX polish. (1) Per-OIDC-provider ForceHttps toggle: when set, the redirect_uri scheme is pinned to https regardless of forwarded headers. Fixes Cloudflare Tunnel / some Caddy setups that terminate TLS upstream without propagating X-Forwarded-Proto, where the IdP would reject the http:// redirect_uri. Host trust boundary stays intact (only the scheme is overridden) so the existing redirect_uri-poisoning defence against attacker-sent X-Forwarded-Host on untrusted peers is preserved. Wired through model, controller upsert DTO, redirect-URI builder, admin form checkbox, and 8-language i18n. 4 new xUnit tests including the untrusted-host regression case. (2) OIDC Token Exchange (RFC 8693-style): new anonymous endpoint POST /TwoFactorAuth/Oidc/Exchange/{providerId} accepts an id_token from a native client (Swiftfin, Findroid, Tizen apps) that ran its own auth-code+PKCE flow against the IdP with our configured ClientId. Server re-verifies signature, issuer, audience=ClientId, expiry. Nonce check is skipped because the native client used its own nonce - the other id_token protections all still apply. Returns a one-shot bridge token the native client POSTs to /Users/AuthenticateByName, identical to the browser callback flow. Same IP-ban + 20/5min rate limit + per-user IP allowlist gates as /Oidc/Login. OidcService.VerifyIdTokenAsync's expectedNonce is now nullable; extracted FinalizeSignInAsync helper holds the shared post-id_token pipeline (userinfo merge, AMR enforcement, group allowlist, user resolution, SsoLink persistence, AuthenticationProviderId reassignment) so both CompleteAsync and ExchangeIdTokenAsync use the same code path. (3) Language picker selection-state bugfix: renderLanguagePicker rendered synchronously during each page's inline init, but _lang was still 'en' at that point because loadTranslations() is async. The value updates to _lang once translations resolve. (4) admin-script.js stays [AllowAnonymous]: an earlier internal experiment with [Authorize] broke the script load because same-origin