v2.5.1
v2.5.1
OIDC enhancements + i18n bugfixes + admin UX polish. In-place upgrade. 254/254 tests pass.
OIDC
-
Per-provider
ForceHttpstoggle — pins the OIDCredirect_urischeme tohttpsregardless of forwarded headers. Fixes Cloudflare Tunnel and some Caddy / nginx setups that terminate TLS upstream but don't propagateX-Forwarded-Proto, where the IdP would reject thehttp://redirect_uri. The X-Forwarded-Host trust boundary is untouched — only the scheme is overridden, so the existingredirect_uri-poisoning defence against attacker-sentX-Forwarded-Hoston untrusted peers is preserved. Wired throughOidcProvidermodel, controller upsert DTO,OidcRedirectUriBuilder, admin form checkbox, and 8-language i18n. 4 new xUnit tests including the untrusted-host regression case. -
OIDC Token Exchange endpoint (RFC 8693-style): new anonymous endpoint
POST /TwoFactorAuth/Oidc/Exchange/{providerId}accepts anid_tokenfrom a native client (Swiftfin, Findroid, Tizen apps) that ran its own auth-code+PKCE flow at the IdP with our configuredClientId. Server re-verifies signature, issuer, audience=ClientId, and 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.Internal refactor:
OidcService.VerifyIdTokenAsync'sexpectedNonceparameter is now nullable, and a new privateFinalizeSignInAsynchelper holds the shared post-id_token pipeline (userinfo merge, AMR enforcement, group allowlist, user resolution,SsoLinkpersistence,AuthenticationProviderIdreassignment) so bothCompleteAsyncandExchangeIdTokenAsyncuse the same code path.
i18n
-
Language picker selection-state bugfix:
renderLanguagePickerrendered synchronously during each page's inline init, but_langwas still'en'at that point becauseloadTranslations()is async. The<select>locked to "English" and never resynced — even hard refresh didn't help because every page-load re-raced. Fix: after rendering, attach_readyPromise.then(syncSelectionFromResolvedLang)so the<select>value updates to_langonce translations resolve. Idempotent for already-resolved promises (subsequent SPA renders). -
Admin Overview i18n race fix: in non-English locales the bundle fetch finished AFTER
renderOverview'sinnerHTMLwrite, thenapplyTranslationsre-walked[data-i18n-key]containers and overwrote the rendered cards back to translated "Loading…". Three-pronged fix:- Gate the initial
renderOverviewonwindow.tfaI18n.readyso the bundle is loaded before the first_tr()call. The ready promise always resolves (even on bundle fetch failure) so this never hangs. - Defensively strip
data-i18n-keyfromfactorsGrid+enrollmentBarsbefore settinginnerHTMLso any laterapplyTranslationssweep skips these containers. - Wire
renderLanguagePicker'sonChangecallback to callrenderOverview()so dynamic content re-renders in the new locale when the user switches language.
- Gate the initial
Admin UX
-
15-second in-memory response cache on
/Dashboard/Overviewkeyed byrange(1w/1m/1y). The endpoint runs the full security-score pipeline (users + audit log + chain verify) + chart build on every hit, so tab switches and range flips would re-pay the bill. With the cache, first load computes (a few hundred ms on a healthy server); subsequent loads within 15s return instantly. Cross-admin (data is server-wide); clears on process restart. -
admin-script.jsstays[AllowAnonymous]with an explicit comment so future CodeQL CWE-285 alerts on it can be dismissed as a false-positive. The named-resource heuristic flags theadmin-prefix but the resource is pure JavaScript UI code shipped to every admin browser anyway — same-origin<script src>tags carry COOKIES but NOT theAuthorization: MediaBrowserheader Jellyfin's auth pipeline checks, so[Authorize]would break the script load and lock the admin page on "Computing…".
CI
update-badge.ymlworkflow: top-levelpermissions: read-all, per-job opt-in forcontents: write. Matches the pattern inrelease.yml/ci.yml/codeql.ymland resolves the OSSF Scorecard Token-Permissions check.update-badge.ymlif:expression now wrapped in double quotes so strict YAML parsers (OSSF Scorecard) don't trip on the colon inside the commit-message literal.
Verification
- 254/254 xUnit tests pass.
- Clean build with
TreatWarningsAsErrors=true— 0 warnings, 0 errors. - OSSF Scorecard: 7.5/10 (was 6.8 during the YAML-error window; restored after the fix).