-
-
Notifications
You must be signed in to change notification settings - Fork 0
Hardening and Options Reference
The operator-facing configuration and hardening options, split out of Provider Setup so the provider pages stay recipe-focused. These are the per-option configuration references — parental rating, provisioning, step-up/MFA, the canonical base URL, the SAML metadata/audience/binding/request-signing/certificate options, the SP metadata endpoint, the test-connection and config export/import endpoints, and the remaining OpenID options. The security-control narratives they lean on (id_token requirements, secrets-at-rest, browser binding, in-flight capacity, SSO-only login) live on the Security Model.
Most of these are set on the OpenID/SAML provider forms or, where noted, directly in the provider's config XML / config API.
Map an identity-provider group to a Jellyfin maximum parental-rating score (#736), so a "kids" group
gets a content-rating ceiling straight from SSO. Turn it on with EnableParentalRatingRoles and list the
mappings; each entry is a Score (the ceiling — smaller is more restrictive) and the Roles it applies to.
This is a config-XML / config-API option (not on the provider form yet), and it uses the same role values as
the rest of RBAC (the claim named by RoleClaim for OpenID; the SAML Role attribute).
EnableAuthorization: true # the master switch; the ceiling is applied only when this is on
EnableParentalRatingRoles: true
ParentalRatingRoleMappings:
- Score: 3 # kids: a low content ceiling
Roles: ["kids"]
- Score: 7 # teens: a higher ceiling
Roles: ["teens"]Rules:
-
Most restrictive wins. A login matching several groups gets the minimum score, never the loosest —
so a user in both
kidsandteensabove is capped at3. -
Unmapped never raises the ceiling. A login that matches no mapping (or a malformed/absent claim) leaves
the account's existing
MaxParentalRatingScoreuntouched — it is never widened by SSO. - A match is authoritative. When a login DOES match, the mapping wins even over a value an admin set by hand (like the other role-derived grants) — so keep the mappings in sync with the intended policy.
-
The score is your library's own rating scale. Jellyfin scores content by an integer; pick the ceiling
that matches how your libraries are rated.
null/unset means unrestricted. - Validated on save. A negative score, or a mapping with no roles, is rejected before it is persisted.
- Gated by
EnableAuthorizationlike the other role-derived grants, so turning RBAC off leaves the ceiling untouched.
When the identity provider's audience is broader than your intended Jellyfin audience — anyone in the
directory can authenticate, but you only want a subset to have working accounts — the built-in choices
were all-or-nothing: require a login role (a denied user never materializes for you to review) or map
roles (the account is enabled immediately). ProvisionNewUsersDisabled fills the middle: provision
the account, but inert, until you approve it.
-
What it does. With the flag on for a provider, an unknown identity's first successful SSO
login creates its Jellyfin account disabled, with no permissions, and the login is refused
(HTTP 403) with an "Your account is not active. It is awaiting administrator approval." message
instead of a session. Every later login of a still-pending account is refused the same way, and each
deferral is recorded in the audit log (
[SSO Audit] New account provisioned pending approval). - Approving an account. Enable it in the Jellyfin dashboard under Administration → Users (untick Disable this user), and set whatever library/permission access it should have. The next SSO login then mints a session normally. Role/folder mapping still applies on that login if the provider is configured for it.
- Off by default (fail-safe). When the flag is off (the default), a new account is created enabled and a session is minted, exactly as before — no change for existing deployments.
- It only ever disables a brand-new account. An existing or adopted account is never disabled by this path — the write is confined to the one place a genuinely new account is created (a conformance test enforces that), so the control cannot strand an existing admin. The break-glass admin (password-provider-pinned) is likewise untouched.
-
Where to set it. The OpenID provider form exposes it as a toggle
(Provision New Users Disabled (Pending Approval)); for SAML providers set
<ProvisionNewUsersDisabled>true</ProvisionNewUsersDisabled>in the provider's config XML. Pair it with the optional rate limiting on the SSO endpoints (see Security Model) so a broad IdP audience cannot flood the pending-account list.
For an internet-exposed server you often want the identity provider to enforce a specific authentication context — most commonly MFA — or to force a fresh authentication at Jellyfin login. The OpenID provider supports the three standard authorization-request parameters (OIDC Core §3.1.2.1), each sent only when you set it (an empty field leaves the request unchanged, so this is upgrade-safe):
-
Acr Values — space-separated
acr_values, most-preferred first: the authentication-context class references you request (e.g. an MFA reference your IdP advertises, or asilver/goldlevel). Also the allow-listRequire Acrchecks the returnedacragainst. -
Prompt — the
promptparameter, e.g.loginto force re-authentication, orconsent. -
Max Age (seconds) —
max_age: the maximum time since the user's last active authentication;0forces re-authentication.
These three only request a context — a provider may ignore them. To enforce it, enable:
-
Require Acr (off by default, for availability) — a fail-closed gate: every login for the provider must
return an
acrclaim, in the signature-verified id_token, that is within Acr Values. A login whoseacris absent or outside the list is refused with "A stronger authentication level (for example MFA) is required to log in." — no session is minted. This covers both login and account linking.
Setup notes.
-
Require Acrneeds Acr Values set — a save withRequire Acron but noacr_valuesis rejected, so a mis-set cannot silently lock out a userbase (nor silently do nothing). The list doubles as both the requestedacr_valuesand the accepted-acrallow-list, so the value the IdP returns for the level you request must appear in it (check your IdP'sacrmapping — e.g. Keycloak's authentication-flow ACR-to-LoA map, Authelia's1FA/2FA, authentik's authorization-flow ACR). - The break-glass password admin is unaffected by
Require Acr, so a misconfiguredacrcannot lock you out of administration entirely — fix the mapping and the SSO logins resume. - Matching is exact and case-sensitive (acr values are case-sensitive per OIDC Core §2).
-
Where to set it. The OpenID provider form exposes all four under Security & hardening → Step-up / MFA;
for SAML there is no equivalent (SAML uses
RequestedAuthnContext, not tracked here).
By default the plugin builds the OpenID redirect_uri and the SAML base URL from the request Host
header. Behind a reverse proxy that forwards an unfiltered X-Forwarded-Host, a client can influence
that host and point the authorization response — the authorization code included — at another origin.
Exact redirect-URI matching at the provider is the backstop, but it only helps if you register an
exact redirect URI (not a wildcard); many self-hosted providers allow wildcards.
-
Set
BaseUrlOverride(OpenID: the "Base URL Override" field in the admin page; SAML: theBaseUrlOverrideconfig value, like the other SAML options) to your canonical external base URL, e.g.https://jellyfin.example.com. When set, everyredirect_uriand SAML URL is built from it and the requestHostis ignored, so a spoofed host cannot redirect the login. It must be an absolutehttp/httpsURL with no query or fragment; a malformed value is rejected when you save. It overrides the per-provider scheme and port overrides. -
Register the exact callback URLs used by your deployment, matching the override. OpenID may use
<base URL>/sso/OID/redirect/<ProviderName>or the legacy<base URL>/sso/OID/r/<ProviderName>; SAML may use<base URL>/sso/SAML/post/<ProviderName>or<base URL>/sso/SAML/p/<ProviderName>. Register the form your deployment actually uses; do not rely on wildcard redirect URIs. The OpenID provider form now shows the exact redirect URI (with a copy button) as you type the provider name, computed from the Base URL Override when set (else this server's address) — copy that value verbatim rather than hand-assembling it. Behind a reverse proxy, set the override so the shown value matches what the login sends. -
Sub-path deployments: if Jellyfin is served under a path (e.g.
https://example.com/jellyfin), include that path in the override — it becomes authoritative and the request's own path base is not added. Omitting it breaks every/sso/...URL. - If you leave
BaseUrlOverrideblank, the URLs fall back to the requestHost; settingBaseUrlOverrideis the robust mitigation. Relying on the fallback behind a reverse proxy is safe only if Jellyfin's Known Proxies is set and the proxy sends a trustedX-Forwarded-Host(not a client-supplied one). Known Proxies alone only decides which upstreams may supply forwarded headers — it does not by itself make a forwarded host trustworthy. -
Scheme override behind a TLS-terminating proxy (fallback path only): when
SchemeOverridesets a scheme that differs from the one Jellyfin sees — a proxy terminates TLS, so Jellyfin receiveshttpwhile the public site ishttps— the derivedredirect_uriand SAML base use the canonical form, without the default port:https://host, nothttps://host:443(and, symmetrically,http://host, nothttp://host:80). Register that canonical no-port form at your provider. If you previously pinned an explicit:443/:80redirect_urito work around the old port-in-URL behavior, switch it to the no-port form.
Rather than hand-copying the SSO endpoint and the Base64 signing certificate — a mis-wrapped or truncated cert is a common, opaque cause of signature-validation failures — you can have the plugin parse the identity provider's SAML metadata for you (#735). An admin-only, elevation-gated endpoint takes either a metadata URL (fetched by the server) or pasted metadata XML and returns the values to fill in:
POST /sso/SAML/ImportMetadata (admin only)
Content-Type: application/json
{ "Url": "https://idp.example.com/metadata" } # OR
{ "Xml": "<md:EntityDescriptor …>…</md:EntityDescriptor>" }
It parses EntityDescriptor → IDPSSODescriptor and returns:
-
Endpoint→ set asSamlEndpoint(theSingleSignOnServiceLocation — HTTP-Redirect preferred). -
PrimaryCertificate/SecondaryCertificate→ set asSamlCertificateand (for a signing-key rollover)SamlSecondaryCertificate, from theKeyDescriptor use="signing"entries. -
EntityId→ the IdP's ownentityID, returned for reference only. It is not yourSamlClientId: that field is the SERVICE PROVIDER's entity id (the value the plugin sends as the AuthnRequestIssuerand expects as the audience), which you choose and register at the IdP — the metadata does not supply it.
Safety notes:
- Admin-only + SSRF-guarded. The endpoint is elevation-gated, and a fetched URL goes through the same SSRF-hardened transport the OpenID discovery fetch uses, so it cannot be driven to reach internal/loopback addresses.
-
Fail-closed parsing. The metadata is parsed with the same hardening as inbound assertions — no
DTD/DOCTYPE (no XXE, no entity-expansion DoS) and a size bound. Malformed, oversized, or incomplete
metadata (missing
IDPSSODescriptor, endpoint, or signing certificate), or a certificate that does not load, is rejected with a clear message and nothing is applied. - Pre-fill, not lock. The endpoint only returns the values; you review and save them through the normal SAML config (the admin form arrives with #725). Every value stays editable.
By default a SAML response is accepted only if its assertion carries an AudienceRestriction that
names this service provider; an assertion minted for a different audience is rejected (fail
closed). This is what stops a response issued for another service that trusts the same identity
provider from being replayed here. Two per-provider options tune it:
-
SamlAudiencesets the SP entity id theAudienceRestrictionmust contain. Leave it blank to use theSamlClientId(the value the plugin sends as the requestIssuer), which is what most deployments want; set it only when your identity provider is configured to address the assertion to an entity id that differs from the client id. The value is compared trimmed. -
DoNotValidateAudience(off by default) skips the audience check entirely. Enable it only for a provider that cannot emit anAudienceRestrictionmatching this service provider — it removes a fail-closed check, so prefer settingSamlAudiencecorrectly over turning validation off.
Both are set through the SAML provider configuration (the SAML/Add API, like the other SAML
options — there is no admin-UI toggle yet); include them whenever you re-post a provider's config so
a save does not reset them.
Multiple AudienceRestriction blocks: SAML 2.0 core §2.5.1.4 permits an assertion to carry more
than one <AudienceRestriction> element, and the plugin requires this service provider's audience to
be named in every block (AND across blocks), while a single matching Audience anywhere inside
one block is enough for that block (OR within a block). An assertion whose first restriction names a
different service provider and whose second names this one is therefore rejected, even though it
matched one of the blocks — it is not strictly addressed to this service provider. An assertion
carrying no AudienceRestriction at all fails closed the same way (rejected).
Two per-provider SAML options, both off by default, tie a response more tightly to this service
provider. They are set through the SAML provider configuration (the SAML/Add API, like the other
SAML options — there is no admin-UI toggle yet); include them whenever you re-post a provider's
config so a save does not reset them.
-
ValidateRecipient— require the assertion's bearerSubjectConfirmationData/@Recipient(and the ResponseDestinationwhen present) to equal this server's assertion-consumer URL. This stops an assertion minted for a different endpoint — or for a different service provider that shares the identity provider — from being presented here. The Recipient is inside the signed assertion, so it is trustworthy even when only the assertion (not the whole Response) is signed. Caveat: the expected URL is built from the base URL, so this binding is only as strong as host resolution unless you pin it — setBaseUrlOverride(see Canonical base URL) to make it exact. The request-host fallback is only safe if Jellyfin's Known Proxies is set and the proxy sends a trustedX-Forwarded-Host. Treat it as defense-in-depth layered on theAudienceRestriction, signature, replay and time-bound checks, not as a standalone guarantee. -
ValidateInResponseTo— accept only solicited responses: the assertion'sInResponseTomust match anAuthnRequestthis server issued and has not yet consumed (one-time, time-bounded). Enabling this disables IdP-initiated (unsolicited) SSO, which carries noInResponseTo— leave it off if your identity provider starts the login from its own dashboard. It applies to the login flow only (not account linking). The outstanding-request store is in-process, so it requires a single Jellyfin instance or sticky sessions: behind a load balancer without session affinity, the challenge and the response can land on different instances and every solicited login is rejected. It also completes the browser-binding defense: browser binding closes forced login for solicited (SP-initiated) responses, and turning this on refuses the unsolicited ones that binding cannot cover — so together they close the vector for an identity provider that can issue unsolicited responses (see Security Model § Login browser binding).
Some identity providers require the service provider to sign its outgoing AuthnRequest ("signed
authentication requests" / "client signature required"). This is off by default — with it off the
request is sent exactly as before (unsigned), so existing deployments are unaffected. Enable it only
for a provider that demands it.
The plugin sends its AuthnRequest over the SAML HTTP-Redirect binding, so the signature is the
detached query-string signature the binding mandates (SAML Bindings §3.4.4.1): the SigAlg and
Signature parameters are appended to the redirect, computed over the URL-encoded
SAMLRequest/RelayState/SigAlg string with RSA-SHA256 (the same no-SHA-1 allowlist the inbound
response path enforces). An enveloped XML signature is not used, because identity providers ignore one
on a redirect-binding message.
Two per-provider SAML options control it (set through the SAML/Add API, like the other SAML options —
there is no admin-UI toggle yet; include them whenever you re-post a provider's config so a save does
not reset them):
-
SignAuthnRequests— turn request signing on for this provider. -
SamlSigningKeyPfx— the service-provider signing key, as a Base64-encoded, unencrypted PKCS#12 (PFX) blob containing the certificate and its RSA private key. Supply the keypair whose public certificate you have registered with the identity provider as the SP's request-signing certificate. For example, from an existing key and cert:openssl pkcs12 -export -inkey sp-key.pem -in sp-cert.pem -passout pass: -out sp.pfx, then Base64 the file (base64 -w0 sp.pfx). Treated as a secret: it is withheld from every config response (aSAMLprovider fetch returns it asnull) so the private key never reaches the admin browser, and a save that leaves it blank keeps the stored key. It is persisted only to the server's on-disk config, and there it is encrypted at rest (see Secrets encrypted at rest). -
SamlRolloverSigningKeyPfx(optional) — a second service-provider signing key, in the same Base64-encoded PKCS#12 shape asSamlSigningKeyPfx, used only for a zero-downtime rollover of the SP's own signing certificate (see SP signing-key rollover below). It is publish-only:AuthnRequestsare always signed with the primary key, never this one. When it is set, the SP metadata advertises both public certificates so the identity provider trusts either during the overlap. It is treated as a secret exactly like the primary key — withheld from config responses, encrypted at rest, and preserved on a blank save. Leave it blank when you are not mid-rotation: behaviour is then identical to a single signing key.
Fail-closed: if SignAuthnRequests is on but the primary signing key is missing or unloadable, the
login challenge is refused with an error — it never silently falls back to sending an unsigned request,
so an operator who turned signing on cannot get a silent downgrade. A garbage primary or rollover key
is rejected when the provider is saved.
To rotate the service provider's own signing certificate without a hard cutover that would break logins the moment the identity provider stops trusting the old certificate, publish both the old and new public certificates during an overlap window while continuing to sign with the old one, then switch:
-
Stage the new key. Generate the new keypair and set
SamlRolloverSigningKeyPfxto it (leaveSamlSigningKeyPfx— the primary — as the current key). The plugin keeps signingAuthnRequestswith the primary (old) key, but the SP metadata now advertises twoKeyDescriptor use="signing"entries — the old and the new public certificate. - Let the identity provider pick up both certificates. Re-import the metadata (or wait for it to refresh if the IdP fetches it by URL) so the IdP trusts signatures from either certificate. There is no downtime: logins keep verifying against the old certificate throughout.
-
Promote the new key. Move the new key into the primary field — set
SamlSigningKeyPfxto the new key. The plugin now signs with the new key, which the IdP already trusts from step 2. -
Return to a single certificate. With the primary now equal to the staged key, the metadata
automatically collapses back to a single
KeyDescriptor(the duplicate is dropped), so no separate "clear the rollover key" step is needed. Once the IdP has re-imported this final metadata, only the new certificate is trusted and the rotation is complete.
Because the rollover key is publish-only and withheld from config responses, it can only be set or changed by posting a non-blank value; a blank save keeps whatever is stored, so an unrelated edit cannot end the overlap window by accident.
The counterpart to the SP-side rollover above: when the identity provider rotates its own
signing key, a hard cutover would break every login the moment it starts signing with the new key while
the plugin still trusts only the old SamlCertificate. To roll over with no downtime, the plugin accepts
a response whose signature verifies against either the primary SamlCertificate or an optional
second certificate:
-
SamlCertificate— the identity provider's public signing certificate, as a Base64-encoded (DER) X.509 certificate. This is the primary and only required certificate. -
SamlSecondaryCertificate(optional) — a second identity-provider public signing certificate, in the same Base64-encoded (DER) X.509 shape. A response is accepted when its signature verifies against either certificate, under the same checks as the primary (the no-SHA-1 algorithm allowlist, single-signature/anti-wrapping binding, XXE/DOCTYPE rejection, time-bound, audience and recipient). Leave it blank when you are not mid-rotation: validation is then byte-for-byte the primary-only behaviour. Unlike the SP's own signing keys (SamlSigningKeyPfx/SamlRolloverSigningKeyPfx, which carry a private key and are encrypted at rest), these are the identity provider's public certificates — not secrets — so they are stored and returned in the clear.
Expired certificates are rejected. A certificate is used to verify only while it is within its own
[NotBefore, NotAfter] validity window (with the same five-minute clock-skew tolerance the other SAML
time-bounds use); an expired (or not-yet-valid) certificate never verifies a response, whether it is the
primary or the secondary. This is what makes the overlap window terminate: once the identity
provider's old certificate expires, it stops authenticating logins on its own.
Upgrade note. Earlier versions did not check the certificate's validity dates at all (XML-DSig verification ignores them), so a deployment whose identity-provider signing certificate has already expired — but whose key still signs — kept working. From this version such a certificate is rejected. If logins for a SAML provider start failing right after upgrading, the primary
SamlCertificateis likely expired: obtain the identity provider's current certificate and set it (inSamlCertificate, or inSamlSecondaryCertificateduring a rotation).
Both are set through the SAML provider configuration (the SAML/Add API, like the other SAML options —
there is no admin-UI toggle yet); include them whenever you re-post a provider's config so a save does
not reset them. A garbage secondary certificate is rejected when the provider is saved, exactly like the
primary.
To roll the identity provider's signing key over:
-
Stage the new certificate. Before the identity provider cuts over, obtain its new public
signing certificate and set
SamlSecondaryCertificateto it (leaveSamlCertificate— the primary — as the current certificate). Logins now verify against either, so responses signed with the old or the new key are accepted throughout the overlap. - Let the identity provider cut over. When the provider starts signing with its new key, logins keep working — they now verify against the secondary. There is no downtime.
-
Promote the new certificate. Move the new certificate into the primary field — set
SamlCertificateto the new certificate — and clearSamlSecondaryCertificate. Validation is back to a single certificate (the new one), and the rotation is complete.
The plugin can publish standard SAML 2.0 service-provider metadata for a provider, so you can register this service provider at your identity provider by URL instead of typing the entity ID, assertion-consumer URL and signing certificate by hand:
GET <base URL>/sso/SAML/metadata/<ProviderName>
for example https://jellyfin.example.com/sso/SAML/metadata/keycloak. The response is an
application/samlmetadata+xml EntityDescriptor/SPSSODescriptor carrying:
- the entityID, which is the provider's configured client ID (
SamlClientId) — the same value the plugin sends as theAuthnRequestIssuer, so the identity provider correlates the two; - one HTTP-POST
AssertionConsumerServiceat<base URL>/sso/SAML/post/<ProviderName>; and - a
KeyDescriptor use="signing"carrying the SP's public request-signing certificate — but only when request signing (SignAuthnRequests) is enabled for that provider. Only the public certificate is published; the private key (SamlSigningKeyPfx) never leaves the server. When signing is off, noKeyDescriptoris emitted andAuthnRequestsSigned="false". During an SP signing-key rollover — whenSamlRolloverSigningKeyPfxis also set — two signingKeyDescriptorentries are published (the primary and the rollover public certificate), so the identity provider trusts either while you swap; again only the public certificates are ever published.
The endpoint is anonymous — SP metadata is public information, and a real identity provider fetches it unauthenticated.
The published entity ID and ACS URL come only from the configured
canonical base URL (BaseUrlOverride), never from the
request Host header. This is deliberate and security-critical: metadata is consumed by the identity
provider to decide where it POSTs assertions, so deriving the ACS from a spoofable (proxy-forwarded)
host would let an attacker point your identity provider at an attacker-controlled endpoint. Therefore the
endpoint fails closed — it returns 409 Conflict and publishes nothing — when the provider has no
BaseUrlOverride set (or no client ID, or request signing is on but the primary — or a configured
rollover — signing key cannot be loaded; a broken KeyDescriptor is never published). Set the
provider's BaseUrlOverride first, then fetch its metadata.
Each provider can be validated for connectivity and basic config before a user hits the failure at first login. In the plugin's admin config page, the OpenID provider form has a Test Connection button that runs the check against the saved provider and shows the result inline; the same checks are also reachable directly:
GET <base URL>/sso/OID/Test/<ProviderName>
GET <base URL>/sso/SAML/Test/<ProviderName>
Both endpoints require administrator privileges — unlike the anonymous provider-name listings, a test
makes the server fetch an admin-configured URL, so it is elevation-gated so that an unauthenticated caller
cannot use it as a server-side request probe. Each returns a small JSON { "Ok", "Message", "Details" }
result:
-
OpenID reads the provider's discovery document through the same hardened path the login uses —
the provider's discovery policy (
RequireHttpsunlessDisableHttpsis set, issuer and endpoint validation), the same bounded fetch — and reports the issuer, the authorization / token / UserInfo endpoints, JWKS reachability (and the advertised signing-key count), and whether the server advertises PKCE (S256) and the RFC 9207 responseissparameter. The client secret is never read for a test (discovery needs no credential) and never appears in the result or the log. -
SAML parses the configured identity-provider public signing certificate (
SamlCertificate) and reports its subject, issuer, validity window and SHA-256 thumbprint. There is no SAML metadata-URL setting, so the SAML test makes no network call; the service-provider signing key (SamlSigningKeyPfx) is never read or reported.
A failing probe returns Ok: false with an actionable, generic message (what to check — reachability,
HTTPS, the .well-known path, a parsable certificate) that never echoes a stored secret. Because the test
reads the stored provider config, save the provider first, then test.
The whole plugin configuration can be exported to a file on one instance and imported into another (for example when replicating a setup or migrating a server). Both actions live in the plugin's admin config page — an Export Configuration button and an Import Configuration button — and are also reachable directly:
GET <base URL>/sso/Config/Export
POST <base URL>/sso/Config/Import
Both endpoints require administrator privileges, like every other config endpoint. The request body of an import is size-capped, so an oversized document is rejected before it is parsed.
The export document is the same redaction the config already applies on the JSON boundary, reused, not
a second copy: the provider secrets — the OpenID client secret (OidSecret) and the SAML signing
keys (SamlSigningKeyPfx, SamlRolloverSigningKeyPfx) — are withheld exactly as they are on a normal
config load, and the server-managed account-link maps (CanonicalLinks, CanonicalLinkIssuers) are
omitted entirely. So an exported file contains no plaintext secret, no encrypted ssoenc: envelope, and
no account-link data. The at-rest data-encryption key (sso-secret.key) lives in a separate file and is
never part of the configuration at all. Everything else — every provider's endpoints, client id, RBAC
roles, folder mappings, and the security toggles — is included, so the document is a complete,
shareable description of the setup minus its secrets. The global rate-limit settings are included in the
export for reference, but are not applied on import (see below).
An import is a fail-closed merge, not a blind overwrite:
- It is validated first through the same rules the config-page save uses (a malformed Base URL override, an unloadable SAML certificate or signing key, or a new provider name containing URI-reserved/control characters is rejected), and only a wholly-valid document is applied — atomically, so a rejected import changes nothing.
- For a provider that already exists on the target instance and whose identity is unchanged, the target's own secret is kept (the export carried none, so the blank value means "keep the stored secret"), and its server-managed account links, issuer bindings, and redirect-path state are preserved.
- Changing an OpenID provider's identity on import drops that provider's links and secret — by design. If the imported document gives an existing OpenID provider a different discovery endpoint or client id, the plugin treats it as a repoint to a potentially different identity provider and, exactly as a config-page edit does (#186), clears that provider's account links and issuer bindings and does not carry over its stored secret. Its users must re-link and an admin must re-enter the secret. This is a deliberate safety measure (a different IdP must not inherit the old one's account mappings), but because the endpoint change can ride in from a file, review an export before importing it over a live provider. SAML provider links are preserved regardless of endpoint changes.
- Providers on the target that are not in the imported document are left untouched (it is a merge, not a replace).
- A provider that is new to the target is added with a blank secret, so its login fails closed until an administrator supplies the secret.
- The global rate-limit settings are not imported — they are instance-local operational tuning (reverse-proxy dependent), so the target keeps its own limiter configuration. This is deliberate: a document from an instance that never enabled rate limiting must not silently turn a DoS control off on a target that had it on. Tune the limiter on the config page, not by import.
Round-trip in practice: export from instance A, import into instance B, then on B open each provider, re-enter its client secret / signing key, and save. The export deliberately never carried the secrets, so re-entering them on the target is the final step — everything else is already in place.
A few remaining per-provider OpenID settings, set through the config XML (no admin-UI toggle yet):
-
DisableHttps(off by default) turns off the requirement that the discovery document — and the endpoints it points at (JWKS, token) — be fetched over HTTPS (options.Policy.Discovery.RequireHttpsin the OpenID login path). With it on, the id_token's signing keys are fetched over a channel a network attacker can intercept, so only turn it on for a local/testing identity provider that genuinely has no TLS — never for a production deployment. Enabling it is recorded in the[SSO Audit]log alongside the other insecure toggles (see OpenID Connect id_token requirements). -
DoNotLoadProfile(off by default) skips the UserInfo endpoint call after token exchange (LoadProfileon the OpenID client). Some providers only return certain claims from UserInfo, not the id_token, so turning this on can leave role/username/avatar mapping without data it expects — set it only for a provider whose id_token alone already carries every claim you map. -
DefaultProvidersets the value the plugin writes into Jellyfin's ownAuthenticationProviderIdfield on the user record after a successful login through this provider (theSessionMinter). This is a Jellyfin-native user attribute, not something the SSO plugin reads back to resolve SSO logins — those always resolve through the per-provider canonical-link maps regardless of this value. Leave it blank to leave the field untouched.
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)