SSO login fails with "Invalid or expired state" across replicas: OAuth state held in-process by the SSO plugin #27
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
SSO login fails intermittently with "Invalid or expired state" on the 2-replica
cheeztvandfafflixdeployments. LDAP login is unaffected; this is browser SSO only.The
SSO-Authplugin (9p4/jellyfin-plugin-sso, 4.0.0.4, baked into the image) generates the OAuthstateand holds it in-process — it exposes a/SSO/OID/Statesendpoint consistent with an in-memory dictionary, with no shared backing store./sso/OID/startlands on whichever pod the load balancer picks; Authentik redirects the browser back to the callback URL, which can land on the other pod. That pod has never seen the state and rejects the login.Reproduced in production on
fafflix, both replicas serving legs of the same login:sessionAffinity: ClientIPis set on the Services but does not help and should not be read as evidence that stickiness is handled here: traffic arrives via the gateway, so every request carries the gateway's source IP rather than the end user's.ClientIPaffinity can't distinguish users behind the same gateway, and with multiple gateway pods it doesn't even pin consistently pod-to-pod.Same class of bug as the in-process single-instance assumptions tracked in #12, but the offending state lives in a third-party plugin rather than in Jellyfin itself, so it doesn't fit as a child of that tracking issue.
Proposal
Fork
9p4/jellyfin-plugin-ssoand back the OAuth state with valkey — the same store the fork already uses for the transcode session store and the scan-leader lease (Jellyfin__TranscodeStore__RedisConnectionString). Any pod can then validate a state generated by any other pod.The fork also has to close a second, unrelated gap: SSO-Auth 4.0.0.4 targets the 10.11 plugin ABI and has no build targeted at 12.0. It currently loads on 12.0 only because Jellyfin's version gate checks
10.11.0.0 < 12.0.0.0. It loads, registers its 27 routes, and serves its config page — but the Jellyfin-facing calls (user provisioning,AuthenticateWithProvider, session-token minting) have never actually been exercised on 12.0, because every login so far has failed earlier in the flow on the state check. Upstream has been unchanged since 2026-05-12, with no PR or issue open for Jellyfin 12 support. The fork needs its own 12.0-targeted build, not just a version-gate pass.Rejected alternatives:
Downstream work once the fork exists:
unkin/jellyfin-ha'sDockerfile.runtimecurrently fetches upstream9p4/jellyfin-plugin-sso, sha256-pinned, via the artifactapi github-proxy remote, anddocker-entrypoint.shsyncs it into the shared RWX/config/plugins. Both need repinning to the fork's release artifact.Acceptance
/sso/OID/startand callback legs deliberately forced onto different pods.AuthenticateWithProvider, and session-token minting.Same class as #12; tracked separately because the fix lives in a forked third-party plugin, not in Jellyfin.
Resolved. Fork
unkin/jellyfin-plugin-sso5.0.0.0, shipped injellyfin-hav0.3.3.Acceptance, verified against
fafflixpod logs:Auth request created.... Strongest case: state created onfafflix-0at 22:51:32 (its first ever SSO activity), redirect leg served byfafflix-1at 22:51:42 after an 8-hour gap with no SSO traffic, auth leg back onfafflix-0. The state cannot have been local tofafflix-1. Attempt 3 is the mirror (1 → 0 → 1). Old code would have failed twice in that sequence.sessionAffinity: ClientIPremoved from both Services;spec.sessionAffinity: Nonelive.Loaded plugin: SSO-Auth 5.0.0.0fromSSO Authentication_5.0.0.0,targetAbi 12.0.0.0. NoInvalidCastException/MissingMethodException.SSO user benvin doesn't exist, creating...→SSO user link doesn't exist, creating...→Auth request created...→Creating new access token. Provisioning and token minting executed on 12.0.Invalid or expired stateappears nowhere in any pod log;InMemorySsoStateStorenever selected.Two corrections to this issue's premises, for the record:
AuthenticateWithProviderdoes not exist in the plugin or onISessionManagerin 12.0. The call is_sessionManager.AuthenticateDirect.POST /sso/OID/Auth/{provider}reads it as well, and the shared item includes the PKCECodeVerifier.Not covered:
cheeztvreceived no SSO traffic during testing, so its store selection is untested rather than failed. Same image and config asfafflix.