github ZL154/JellyfinSecurity v1.3.1
v1.3.1 — Per-device scoping, working app passwords, QR pairing, audit hardening

latest releases: v2.6.0, v2.5.22, v2.5.21...
4 months ago

Critical fixes

Failing 2FA no longer signs you out everywhere

Pre-verification and blocking were user-scoped. When a new device tried to sign in and got blocked, EVERY other session for that user hit a 401. Now scoped to (user, device) — only the device that actually failed gets blocked.

Swiftfin / native-client bypass closed

The 2-minute verification window after web 2FA login was user-scoped, which silently granted Swiftfin/TV sessions a free pass if they connected within the window. Now scoped to the specific (user, device) that completed 2FA. The target device still has to pair or use an app password to skip 2FA.

App passwords now actually work for native clients

Implemented as a fast path in IAuthenticationProvider instead of trying to intercept Jellyfin's auth response. Submit the app password in the native app's password field — server accepts it and skips the default provider.

  • Rate-limited (10/min/IP) BEFORE PBKDF2 work
  • Lockout-gated (account lockout applies first)
  • Race-checked (revoke-during-auth closes cleanly)
  • Flat-timing probe even on empty lists

New features

QR pairing

Each pending device has a "QR" button. Click it, scan the code with any phone already signed in with 2FA, confirm — that device is now paired and bypasses 2FA on future sign-ins.

Back button on challenge page

Because "no back button on 2fa screen" was a real complaint.

Broader settings tile selectors

"Two-Factor Auth" now also appears on the user preferences page in themed Jellyfin skins (STARTRACK etc.).

Security audit hardening (second pass)

  • Atomic pending-pair approval: single-consume before PairedDevices mutation; concurrent duplicate approvals safe
  • Deviceless MarkDevicePreVerified is a no-op (was a latent user-wide bypass)
  • WipeAllForUser clears pre-verified + quick-connect + blocked state on 2FA disable (previously only blocked state was cleared)
  • AuditResult.ConfigChanged distinguishes config changes from auth bypasses in the audit log
  • Audit entries added for self-disable, pending-pair deny, paired-device revoke, session revoke
  • PBKDF2 verify bounds iterations (50k–2M), salt (8–64), hash (=32)
  • Middleware restricted to explicit auth paths instead of shape-only detection
  • Dead code removed

Upgrade

Install over v1.3.0. Restart Jellyfin. Hard-refresh browsers (Ctrl+Shift+R) so the new inject.js loads.

What to test

  1. Sign out gotcha gone: sign in on laptop + phone + TV, then type a wrong 2FA code from a new device → only that new device gets 401'd.
  2. Swiftfin passwordless: device pairing flow unchanged; the first failed login creates a pending pairing, approve from Setup, retry.
  3. Swiftfin with app password: generate app password in Setup → paste into Swiftfin's password field → signs in.
  4. QR pairing: failed native-client sign-in → Setup → "QR" button on the pending row → scan on phone → confirm → done.
  5. Back button on /TwoFactorAuth/Challenge returns you to /TwoFactorAuth/Login.

Don't miss a new JellyfinSecurity release

NewReleases is sending notifications on new releases.