Admin two-step verification (passkeys / authenticator app), and shows with several series

Summary

Two changes:

  1. Two-step verification for Admin. Admin now asks for a passkey or an authenticator app's code. An admin who has neither is walked through setting one up the first time they open Admin.
  2. Shows with several series. Season folders like UK - Season 01 or All Stars - Season 02 used to be merged into the show's own seasons. Each is now its own series. Also fixes episode thumbnails covering the episode title.

Two-step verification for Admin

  • Methods: passkeys (Face ID / Touch ID, Windows Hello, Android, security keys) through .NET 10 Identity's built-in WebAuthn support, and 6-digit authenticator codes (TOTP, e.g. Apple Passwords, Google/Microsoft Authenticator, 1Password). A user can have both, and several passkeys.
  • Scope: only Admin asks. A check lasts AdminMfa:Hours (default 12) and applies however the admin signed in, including Google. Signing in itself is unchanged.
  • First-time setup: confirm the password, create a passkey or scan the QR code (the otpauth:// link opens Apple's Passwords app on iPhone/Mac), then save 10 one-time recovery codes.
  • Profile → Two-step verification: add/remove passkeys, turn the authenticator on/off, make new recovery codes. Removing a method or making new codes needs a recent check, so a stolen session can't swap in its own.
  • Recovery: Admin → Users has Reset two-step for another user. AdminMfa:Required = false lets a lone admin back in.
  • Brute force: wrong codes count toward the existing lockout (5 → 15 minutes), on top of the existing auth rate limit.

Implementation notes

  • Where the check is kept: the time of the last check (and of a just-confirmed password) is stored as a claim in the sign-in cookie. SecurityStampValidatorOptions.OnRefreshingPrincipal copies it over when the cookie is rebuilt every minute, and the cookie is re-issued after anything that changes the security stamp.
  • Own authenticator flag: the authenticator uses its own AppUser.AuthenticatorEnabled, not Identity's TwoFactorEnabled. Setting the latter makes PasswordSignInAsync require a code at every sign-in, which broke sign-in during testing.
  • Admin endpoints: guarded by an endpoint filter that returns 403 with { mfa: "setup" | "verify" }, which the page uses to show the setup or the check.
  • New dependency: QRCoder (MIT), which draws the authenticator QR code as an SVG data: image, allowed by the existing CSP.

Shows with several series

  • Season folders: <series> - Season NN, <series> - SNN and <series> Season NN are now that series' seasons (Season.Series).
  • Loose files: files in the show's own folder whose name isn't the show's (drag.race.espana.s01e01.mkv in Ru Pauls Drag Race) get their own series too.
  • Show page: picks the series first, then its seasons. Play all and next/previous episode go series by series, starting with the show's own.
  • Existing libraries: episodes are refiled from their path at the next scan, without re-reading the files.
  • Thumbnails: episode frames now fill and crop to their column instead of drawing at full size over the title.

Migrations

  • TwoStepVerification: adds the AspNetUserPasskeys table (Identity schema version 3) and the AspNetUsers.AuthenticatorEnabled column.
  • ShowSeries: adds the Seasons.Series column (default "") and widens the unique index to (ShowId, Series, Number).

Neither rebuilds an existing table.

Deployment notes

  • Settings: no settings changes are required. AdminMfa defaults to Required: true, Hours: 12.
  • Passkeys and the proxy: passkeys need the HTTPS address, and MediaRiffle must see requests as HTTPS behind the proxy. That means the proxy in TrustedProxies and X-Forwarded-Proto: https set on the HAProxy frontend. Authenticator codes work either way.

Testing

  • Automated: 113 tests pass. New tests cover the check-time logic and its survival of the cookie refresh, the authenticator link/key/QR, and series parsing against real folder names.
  • Manual, against a running server: authenticator setup end to end (codes generated per RFC 6238); the check surviving the one-minute security stamp refresh; recovery codes working once; removal needing a check; lockout after 5 wrong codes; sign-in unaffected by an enabled authenticator; passkey options returned correctly.
  • Scan: a sample multi-series show split into the right series.
  • Not tested yet: creating and using a passkey in a real browser, and the new show page and setup screens in a browser.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading