-
-
Notifications
You must be signed in to change notification settings - Fork 0
Single Logout Design
Status: NOT IMPLEMENTED. This document records the constraints and the
intended design so a future single-logout implementation starts from a
threat-modelled position. No logout endpoint, logout-token receiver, or
LogoutRequest issuer exists in the plugin today. This is a design record only
(issue #154); building SLO is a separate, threat-modelled work item.
SLO of any flavour needs artifacts from the login that the plugin currently does not keep. The login pipeline verifies a protocol response, projects it to a small protocol-agnostic identity, and discards the raw protocol material:
-
OIDC — the raw
id_tokenis discarded after the callback. In theOidcLoginService(Flows module) (CallbackAsync),result.IdentityToken(the raw id_token fromProcessResponseAsync) is read for exactly two purposes: the RFC 9207 response-issmix-up check (OidcResponseIssuer.IsRejected(..., result.IdentityToken, ...)) and to derive the account-link issuer (OidcResponseIssuer.IdTokenIssuer(result.IdentityToken), which only extracts theissclaim — theOidcResponseIssuerin the Oidc module). The token itself is never stored. It does not survive intoAuthorizeSession/VerifiedIdentity. -
VerifiedIdentity(the keystone carried into session minting) has no token field. TheVerifiedIdentity(Identity module) carries onlySubject,Issuer(the id_tokenissstring, not the token),Username,EmailVerified, and the privilege flags — noid_token, nosid, no SAMLSessionIndex. -
SAML — no
SessionIndexis captured. The assertion parser (theSamlparser type at the project root) exposesGetNameID(),GetInResponseTo(), andXml, but there is noGetSessionIndex()accessor.SamlLoginService.AuthenticateAsynckeeps onlyInResponseTo(for browser-binding correlation) in the in-flightSamlLoginOutcome; theNameIDsurvives asVerifiedIdentity.Subject. A SAMLLogoutRequestrequires both theNameIDand theSessionIndexof the session being terminated — the latter is not retained. -
The OIDC
end_session_endpointis read but never persisted. theOidcDiscoveryReader(Oidc module) mapsdiscovery.EndSessionEndpointinto the in-memoryProviderInformation, but that object lives only for the duration of a single login round-trip. Nothing writes the end-session endpoint anywhere durable. -
The account-link store persists no per-session data. Canonical links
(the
CanonicalLinkServicein the Linking module) key onprovider + subject (+ issuer for OIDC)→ Jellyfin user id. There is no mapping from an SSO subject/session to a concrete Jellyfin session or device, and no place a logout handler could look up "which Jellyfin sessions belong to this SSO login." -
In-flight stores are transient and capped.
OidcStateStoreandSamlOutcomeStore/SamlRequestCacheare process-wide, in-memory, time-bounded (~15 min), and CSPRNG-keyed. They are login-correlation caches, not a session ledger; they are gone on restart and are the wrong home for anything a logout needs minutes-to-hours later.
Consequence: SLO is cheap to plan for now and expensive to retrofit. The one prerequisite worth deciding now is what to persist per Jellyfin session at login time (see the OIDC and SAML sections). Persisting it later means it is simply absent for every session created before the change.
The plugin multi-targets, and the OidcClient version differs per TFM
(the plugin's SSO-Auth.csproj): OidcClient 6.0.1 on net9.0 (Jellyfin 10.11)
and OidcClient 7.1.0 on net10.0 (Jellyfin 12.0). Any reliance on
7.1.0's LogoutRequest/EndSessionEndpoint modelling must be conditioned on
the target — the net9.0 build cannot assume the 7.x logout surface. RP-initiated
logout is a plain front-channel redirect and can be built without the library
either way.
-
Back-channel logout (OP → RP
logout_token) is the target. The OP POSTs a signedlogout_token(a JWT) to a plugin-hosted back-channel logout URI; the plugin validates it and terminates the corresponding Jellyfin session(s) server-to-server. No browser, no third-party cookies. -
Front-channel logout is effectively dead and is ruled out. The
OpenID Front-Channel Logout model relies on the OP loading per-RP
<iframe>s in the user's browser; under third-party-cookie blocking (now default in major browsers) those iframes do not carry the RP session cookie, so the notification silently fails. The spec itself warns of this. Do not build front-channel iframe logout. -
Reachability caveat for LAN-only installs. Back-channel logout requires the
OP to reach the plugin's logout URI. Jellyfin is frequently deployed
LAN-only / behind NAT with no inbound path from a public OP. Back-channel is
therefore best-effort: when the OP cannot reach the RP, no
logout_tokenarrives and the Jellyfin session is not terminated by SLO. The design must treat a missing logout notification as "SLO did not happen," never as "logout succeeded" — and must not present LAN-only operators a guarantee it cannot keep.
-
RP-initiated logout (user clicks "log out", the RP redirects the browser to
the OP's
end_session_endpoint) is the realistic complement to best-effort back-channel, and works through the browser without third-party cookies. -
It wants an
id_token_hint.end_session_endpointtakesid_token_hintso the OP can identify the session to end and skip a logout-confirmation prompt. Without it many OPs either refuse or force an interactive confirmation. -
The plugin does not retain the id_token today (see grounding facts). To send
id_token_hint, the plugin must persist the raw id_token (or, as a minimum,iss/sid/sub, which cover session correlation and back-channellogout_tokenmatching but do not satisfyid_token_hint, which wants the full JWT) against the Jellyfin session at login time.
Security cost of storing the id_token (must be honoured, fail-closed):
- A stored id_token is sensitive: it is a signed assertion of identity and
claims. Treat it like the client secret and signing keys already are —
encrypted at rest via the existing
Secretsenvelope (#158, the same pathOidSecret/SamlSigningKeyPfxuse), never logged, redacted on export. -
Scope-minimise. Prefer persisting only
iss+sid+subwhen the OP supports back-channel logout (that is all alogout_tokenneeds to match), and store the full id_token only when RP-initiatedid_token_hintis actually a configured requirement for that provider. -
Expiry. Bind the stored token/identifiers to the Jellyfin session lifetime
and purge on session end; do not keep an id_token past the session it belongs to.
An expired id_token is still a valid
id_token_hintforend_sessionper spec, so retention is a storage-lifetime decision, not anexpdecision — but never keep it beyond the session. - Persist it per Jellyfin session, so logout can map an SSO logout signal to the specific session(s) to kill (see "Jellyfin session invalidation" below).
-
Back-channel (SOAP) vs front-channel (HTTP-Redirect/POST). SAML SLO exchanges
a
LogoutRequest/LogoutResponse. Back-channel uses SOAP directly between SP and IdP (no browser, same reachability caveat as OIDC back-channel — an inbound path from the IdP to the plugin is required, often absent on LAN-only installs). Front-channel uses browser redirects/POST and is subject to the same third-party-context erosion, though SAML front-channel is a top-level navigation rather than a hidden iframe, so it degrades less severely than OIDC front-channel — it is a usable fallback, not dead. -
NameID+SessionIndexretention. Building aLogoutRequestrequires theNameIDand theSessionIndexof the authenticated session. TheNameIDis retained today (asVerifiedIdentity.Subject); theSessionIndexis not captured at all — theSamlparser type has no accessor for it and nothing stores it. Capturing and persisting theSessionIndex(alongsideNameID) per Jellyfin session at login is the SAML prerequisite, symmetric to the OIDC id_token/sid retention. - The IdP's
SingleLogoutServiceendpoint is not in the config today (SamlEndpointis the SSO endpoint only — thePluginConfigurationin the Config module); SLO needs a separately configured logout endpoint (and, for signed logout, reuse of the existing request-signing key material).
Logout messages are security-relevant inbound requests and must be validated with the same rigour as login — an unauthenticated "log this user out" endpoint is itself an attack surface.
-
Spoofing / Tampering (forged logout messages). An OIDC
logout_tokenand a SAMLLogoutRequestmust be validated exactly like a login assertion: signature verified against the provider's keys,iss/issuer bound to the configured provider, audience/recipient checked, and timing/iatbounded. Reuse the existing validators' posture (OidcIdTokenValidator,SamlAssertionValidator) — do not hand-roll a laxer path for logout. Alogout_tokenadditionally must carry theeventsclaim and MUST NOT carrynonce(per the back-channel spec); reject non-conformant tokens fail-closed. -
Replay. Logout messages must be one-time, mirroring the login replay caches
(
jtiforlogout_token; the SAML replay cache forLogoutRequest). A replayed logout that merely re-kills an already-dead session is low-harm, but the validation path must not become a weaker oracle than login. -
Elevation / DoS — "logout as an unauthenticated session-kill vector." The
single most important threat: a logout endpoint must never let an unauthenticated
or cross-user caller terminate an arbitrary user's session. A logout only
terminates a session when the presenter proves control of the SSO session
being ended (a validly-signed
logout_token/LogoutRequestfrom the bound provider that correlates to a stored session identifier). RP-initiated logout triggered from the browser must be CSRF-protected and may only end the caller's own session. Fail closed: unresolvable correlation → terminate nothing, not everything. -
Repudiation / audit. Every SLO-driven session termination should be audited
the way logins are (
SsoAudit), so an operator can see that a session ended via SLO and why. - Availability / mass-logout. A malformed or storm of logout signals must not become a mass-lockout: throttle like the login rate-limit gate, and never let one logout signal fan out to kill unrelated sessions. Losing the correlation store on restart must degrade to "SLO no-op" (sessions simply live out their normal Jellyfin lifetime), never to "kill all sessions."
- Jellyfin sessions are minted through
ISessionManagerin theSessionMinter(Session module) (AuthenticateDirect); the plugin holds no mapping from an SSO subject/session to the resulting Jellyfin session/device. - To act on a logout signal, the design needs a durable
(provider, iss/sub, sid | SessionIndex)→ Jellyfin session/device map, written at mint time. Terminating the session then means invalidating that Jellyfin session/token through the server's session API — the exact mechanism (and whether the plugin may revoke a session it did not itself own) is an open design question to resolve against the Jellyfin session API at implementation time, empirically, not by assumption.
- Fail-closed throughout. Missing/invalid signature, unresolvable session correlation, unreachable OP/IdP, or an unconfigured logout endpoint → terminate no session and surface the condition; never default to "logged out" and never fan out to unrelated sessions.
-
Scope of retained material is minimised and encrypted at rest. Store the
least that satisfies the enabled logout mode (prefer
iss/sid/suband SAMLSessionIndex; full id_token only whenid_token_hintis required), always via the existing at-restSecretsenvelope, never logged, purged on session end. - Best-effort, honestly presented. Back-channel and SOAP SLO depend on OP/IdP → plugin reachability that LAN-only installs usually lack; the feature must not claim a logout guarantee it cannot deliver in those topologies.
-
Not implemented yet. This document records constraints only. The retention
decision (persist
iss/sid/suband SAMLSessionIndexper Jellyfin session at login) is the one cheap-now / expensive-later prerequisite to settle before or alongside the first SLO code.
Repository · Issues · Releases · Security policy — report vulnerabilities privately, never in a public issue. Pages describe what is implemented today; if the wiki disagrees with the code, the code wins.
Getting started
- Installation
- Provider Setup
- Hardening & Options Reference
- Migrating from 9p4
- Troubleshooting
- Rollback
How it works
Security
- Security Model
- Security Conformance (ASVS / RFC 9700)
- Threat Model
- SSO-Only Login — design record
- Single Logout — design record
Standards & process (internal / maintainer)