-
-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
This plugin is the login path for your Jellyfin server, so it is built to fail closed: anything the plugin cannot positively verify is rejected, never waved through. This page describes the protections implemented today; it grows as the hardening program continues.
An SSO login is bound to the identity provider's stable identifier — OpenID sub /
SAML NameID — recorded as a canonical link. A first login whose name merely matches a
pre-existing, unlinked Jellyfin account is refused (HTTP 403), not silently adopted,
unless an administrator explicitly enables AllowExistingAccountLink for that provider.
This prevents a user whose provider-supplied name equals an existing account (e.g.
admin) from taking that account over.
A SAML response is accepted only when all of the following hold; any failure rejects the login:
-
Signature — exactly one signature, whose reference covers the element that is
actually consumed (root
Responseor the singleAssertion). This blocks XML-signature-wrapping ("verify one node, read another"). -
XML parsing — the response is parsed with DTD processing prohibited, so any
<!DOCTYPE>is rejected before any other check runs. This closes both external-entity injection (XXE) and internal-entity expansion ("billion laughs") in the untrusted response. -
Time bounds —
NotOnOrAfter(andNotBeforewhen present) are enforced with a bounded clock skew. A missing or unparseable bound is rejected (it is not treated as "valid forever"). -
Audience — the assertion's
AudienceRestrictionmust name this service provider (configurable; opt-out available for providers that cannot emit it). This stops an assertion minted for a different service from being replayed here. - One-time use — a consumed assertion ID cannot mint a second session within its validity window (per-provider replay cache).
When avatar sync is configured, the URL is derived from identity-provider claims, so the fetch is constrained to public http(s) targets: the resolved address is validated at connect time (each redirect hop too), blocking loopback/private/link-local/CGNAT/ metadata ranges and DNS rebinding, and the download is bounded in time and size.
The in-flight authorize-state store is safe under concurrent logins, so parallel sign-ins cannot corrupt it or throw. A state is also provider-bound and single-use: it is minted for one provider and rejected outright if presented to a different provider's callback (stops a state validated at a low-trust provider from being replayed against a higher-trust one), and it lives for about 15 minutes — long enough for a provider-side MFA or consent step, short enough to bound the replay window.
The id_token returned by the provider is validated before any identity is read from it; any failure rejects the login:
-
Signature — verified against the provider's published JWKS (from its discovery
document). Only asymmetric algorithms are accepted (RS256/384/512, PS256/384/512,
ES256/384/512); a token signed with a symmetric algorithm (HS256) or left unsigned
(
alg: none) is rejected regardless of configuration. A rotated signing key is picked up automatically. -
Issuer — must match the discovery issuer. The per-provider
DoNotValidateIssuerNameescape hatch relaxes only this check; signature, audience and expiry validation have no off switch. -
Audience — must include this client. When an
azpclaim is present it must equal the client id, and a multi-audience token withoutazpis rejected — it may have been minted for a different party that merely lists this client as a co-audience. -
Lifetime —
exp(andnbfwhen present) are enforced with a bounded clock skew. - When the provider sends an
at_hashclaim, it must match the issued access token.
What your provider must be configured to satisfy is described in the id_token requirements.
The rendered auth page (the one that carries the one-time state token or signed assertion
and completes the login client-side) sets X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and Cache-Control: no-store — clickjacking
protection, no MIME sniffing, no token leakage via the Referer header, and no caching of a
token-bearing response.
Security-relevant events — successful logins, opt-in adoption of an existing unlinked
account, and admin provider add/remove — are written as structured [SSO Audit] log
entries (username, provider, admin flag; never a secret or config value).
Report vulnerabilities privately via Report a vulnerability. Security-relevant behavior is covered by the automated test suite, and login-path changes go through an adversarial security review before merge.
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)