Skip to content

Hardening and Options Reference

iderex edited this page Jul 20, 2026 · 1 revision

Hardening & 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.

Parental rating by role (content ceiling)

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 kids and teens above is capped at 3.
  • Unmapped never raises the ceiling. A login that matches no mapping (or a malformed/absent claim) leaves the account's existing MaxParentalRatingScore untouched — 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 EnableAuthorization like the other role-derived grants, so turning RBAC off leaves the ceiling untouched.

Provision new users pending approval

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.

Step-up / MFA

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 a silver/gold level). Also the allow-list Require Acr checks the returned acr against.
  • Prompt — the prompt parameter, e.g. login to force re-authentication, or consent.
  • Max Age (seconds)max_age: the maximum time since the user's last active authentication; 0 forces 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 acr claim, in the signature-verified id_token, that is within Acr Values. A login whose acr is 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 Acr needs Acr Values set — a save with Require Acr on but no acr_values is rejected, so a mis-set cannot silently lock out a userbase (nor silently do nothing). The list doubles as both the requested acr_values and the accepted-acr allow-list, so the value the IdP returns for the level you request must appear in it (check your IdP's acr mapping — e.g. Keycloak's authentication-flow ACR-to-LoA map, Authelia's 1FA/2FA, authentik's authorization-flow ACR).
  • The break-glass password admin is unaffected by Require Acr, so a misconfigured acr cannot 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).

Canonical base URL (recommended hardening)

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: the BaseUrlOverride config value, like the other SAML options) to your canonical external base URL, e.g. https://jellyfin.example.com. When set, every redirect_uri and SAML URL is built from it and the request Host is ignored, so a spoofed host cannot redirect the login. It must be an absolute http/https URL 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 BaseUrlOverride blank, the URLs fall back to the request Host; setting BaseUrlOverride is 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 trusted X-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 SchemeOverride sets a scheme that differs from the one Jellyfin sees — a proxy terminates TLS, so Jellyfin receives http while the public site is https — the derived redirect_uri and SAML base use the canonical form, without the default port: https://host, not https://host:443 (and, symmetrically, http://host, not http://host:80). Register that canonical no-port form at your provider. If you previously pinned an explicit :443/:80 redirect_uri to work around the old port-in-URL behavior, switch it to the no-port form.

Import SAML config from IdP metadata

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 EntityDescriptorIDPSSODescriptor and returns:

  • Endpoint → set as SamlEndpoint (the SingleSignOnService Location — HTTP-Redirect preferred).
  • PrimaryCertificate / SecondaryCertificate → set as SamlCertificate and (for a signing-key rollover) SamlSecondaryCertificate, from the KeyDescriptor use="signing" entries.
  • EntityId → the IdP's own entityID, returned for reference only. It is not your SamlClientId: that field is the SERVICE PROVIDER's entity id (the value the plugin sends as the AuthnRequest Issuer and 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.

SAML audience validation

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:

  • SamlAudience sets the SP entity id the AudienceRestriction must contain. Leave it blank to use the SamlClientId (the value the plugin sends as the request Issuer), 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 an AudienceRestriction matching this service provider — it removes a fail-closed check, so prefer setting SamlAudience correctly 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).

SAML response binding (optional hardening)

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 bearer SubjectConfirmationData/@Recipient (and the Response Destination when 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 — set BaseUrlOverride (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 trusted X-Forwarded-Host. Treat it as defense-in-depth layered on the AudienceRestriction, signature, replay and time-bound checks, not as a standalone guarantee.
  • ValidateInResponseTo — accept only solicited responses: the assertion's InResponseTo must match an AuthnRequest this server issued and has not yet consumed (one-time, time-bounded). Enabling this disables IdP-initiated (unsolicited) SSO, which carries no InResponseTo — 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).

SAML request signing (optional)

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 (a SAML provider fetch returns it as null) 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 as SamlSigningKeyPfx, used only for a zero-downtime rollover of the SP's own signing certificate (see SP signing-key rollover below). It is publish-only: AuthnRequests are 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.

SAML SP signing-key rollover

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:

  1. Stage the new key. Generate the new keypair and set SamlRolloverSigningKeyPfx to it (leave SamlSigningKeyPfx — the primary — as the current key). The plugin keeps signing AuthnRequests with the primary (old) key, but the SP metadata now advertises two KeyDescriptor use="signing" entries — the old and the new public certificate.
  2. 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.
  3. Promote the new key. Move the new key into the primary field — set SamlSigningKeyPfx to the new key. The plugin now signs with the new key, which the IdP already trusts from step 2.
  4. 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.

SAML identity-provider certificate rotation (inbound)

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 SamlCertificate is likely expired: obtain the identity provider's current certificate and set it (in SamlCertificate, or in SamlSecondaryCertificate during 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:

  1. Stage the new certificate. Before the identity provider cuts over, obtain its new public signing certificate and set SamlSecondaryCertificate to it (leave SamlCertificate — 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.
  2. 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.
  3. Promote the new certificate. Move the new certificate into the primary field — set SamlCertificate to the new certificate — and clear SamlSecondaryCertificate. Validation is back to a single certificate (the new one), and the rotation is complete.

SAML service-provider metadata

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 the AuthnRequest Issuer, so the identity provider correlates the two;
  • one HTTP-POST AssertionConsumerService at <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, no KeyDescriptor is emitted and AuthnRequestsSigned="false". During an SP signing-key rollover — when SamlRolloverSigningKeyPfx is also set — two signing KeyDescriptor entries 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.

Test connection (admin)

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 (RequireHttps unless DisableHttps is 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 response iss parameter. 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.

Configuration export / import (admin)

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 is redacted — secrets never leave the server

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).

The import merges and preserves an unchanged provider's secrets and links

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.

Other OpenID options

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.RequireHttps in 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 (LoadProfile on 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.
  • DefaultProvider sets the value the plugin writes into Jellyfin's own AuthenticationProviderId field on the user record after a successful login through this provider (the SessionMinter). 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.

Clone this wiki locally