Skip to content

OIDC SSO

ZL154 edited this page Jun 17, 2026 · 3 revisions

OIDC / SSO sign-in

Let users sign in with Google / Microsoft / Authelia / Authentik / Keycloak / PocketID / Cloudflare Access / any OIDC-compliant IdP instead of (or alongside) a Jellyfin password. Password-less accounts work too — SSO replaces the password.

The plugin does PKCE (S256), id_token signature + issuer + audience + nonce validation, optional AMR-based IdP-MFA enforcement, and optional group allowlists.


How a user is matched on sign-in

When someone signs in via OIDC, the plugin resolves them in this order:

  1. Existing SSO link on a Jellyfin user (matched by the IdP's stable sub) → signs in.
  2. Email returned by the IdP matches a per-user email configured in the plugin → signs in and links for next time.
    • Refused if the email matches more than one Jellyfin user (ambiguous).
    • An administrator is never resolved or auto-linked by email — admins must link OIDC explicitly from their Setup page (matched by the immutable sub).
  3. Auto-create is enabled + nothing matched → a new Jellyfin account is created.
  4. Nothing matched + auto-create off → sign-in refused with "No Jellyfin user matched".

Setting up a Google provider (walkthrough)

1. Register a Google OAuth client

  1. Google Cloud Console → create or pick a project.
  2. OAuth consent screen → External → fill App name / support email → add your Gmail as a test user → Finish.
  3. Credentials+ Create credentialsOAuth client IDWeb application.
  4. Leave Authorised redirect URIs open for now — you'll paste the exact URL in step 3.
  5. Save. Copy the Client ID + Client secret.

2. Add the provider in Jellyfin

  1. Jellyfin admin → Jellyfin SecuritySign-in Methods tab → "Add provider…".
  2. Preset: Google. Paste Client ID + Secret. Username claim: email. Save.

3. Register the redirect URI back at Google

After saving, the provider list shows the exact redirect_uri to register:

https://YOUR-JELLYFIN-HOSTNAME/TwoFactorAuth/Oidc/Callback/<slug>

<slug> is derived from the Display name (e.g. Googlegoogle, Login with Googlelogin-with-google). Go to Google Cloud Console → Credentials → your OAuth client → add this exact URL to Authorised redirect URIs and save.

If the slug doesn't match what's registered, the IdP returns redirect_uri_mismatch and sign-in fails (issue #28). New providers default to ForceHttps=true so the callback is https:// behind a TLS-terminating proxy.

4. Make sure each user has their email configured

  • Each user sets their email on the Setup page (/TwoFactorAuth/Setup), or
  • admin fills it in Jellyfin Security → Users tab's email column (press Tab to save).

5. Done

Sign out — the login page now shows a "Sign in with Google" button. Click → Google consent → bridge page → signed in.

On the native mobile app: the consent has to open in your phone's real browser, because Google rejects its login screen inside an app WebView (disallowed_useragent). The app shows a dialog with "Copy sign-in link" as a guaranteed path if the in-app browser is blocked — see Troubleshooting → Google sign-in fails on the Android app. (v2.5.12)


Other providers

Preset Discovery auto-filled Notes
Google Username claim: email
Microsoft / Entra Replace common in the discovery URL with your tenant ID for single-tenant apps
Apple Returns email only on first sign-in; no email_verified claim
Authelia Paste https://authelia.domain/.well-known/openid-configuration
Authentik Copy the discovery URL from the provider details in Authentik admin
Keycloak https://keycloak.domain/realms/<realm>/.well-known/openid-configuration
PocketID https://pocketid.domain/.well-known/openid-configuration
Cloudflare Access discovery URL ends /cdn-cgi/access/sso/oidc/<app-id>/.well-known/openid-configuration
GitHub / Discord OAuth2 only, not OIDC — not supported

Per-provider options

  • Allowed groups — sign-in refused unless the IdP's groups/roles claim contains at least one of these.
  • Require IdP MFA — refuses sign-in unless the id_token's amr claim indicates MFA (mfa, hwk, otp, sca).
  • Auto-create users — creates a Jellyfin account for unmatched IdP identities. Only enable for IdPs where you trust everyone with an account (not public Google).
  • Skip plugin 2FA — default ON; the IdP already authenticated. Disable for belt-and-braces.
  • Email claim (v2.5.11) — the claim holding the user's email (default email); override only for IdPs that use a custom claim such as internal_email. Used for email-matching and the auto-fill below.
  • Fill email from the IdP (v2.5.11, default on) — on sign-in, copy the IdP email into the Jellyfin user's email field (used for email OTP and shown in the Users tab) when it isn't already set. A manually-entered email is never overwritten.
  • Login button text + icon (v2.5.11) — customise the button label and a logo (https / data: image URL) per provider, instead of the default "Sign in with {name}".
  • Always show the account chooser — adds prompt=select_account so the IdP shows its account picker instead of silently reusing an already-signed-in IdP account. Useful on shared machines / multi-account households.
  • Allow private / VPN / LAN endpoints (Advanced, default off) — see below.

Role-based library access (#65)

Map each IdP group/role to the libraries it grants. On sign-in, a user's enabled libraries become the union of all libraries granted by their matched roles, and "Enable all libraries" is turned off.

  • Per-provider, with an admin-UI editor that lists your libraries.
  • Works only with IdPs that send a groups/roles claim — Authentik, Authelia, Keycloak, and similar. Google does not send groups.
  • Administrators are never restricted.

Setup: Sign-in Methods → edit provider → enable Apply role → library access → add Role → Library rows using the picker.


Profile-picture sync (#66)

Optionally copy the IdP profile picture into the Jellyfin avatar on sign-in.

  • Per-provider; reads the standard picture claim (overridable).
  • Fetched through the same SSRF egress guard as the other IdP calls, content-type-restricted and size-capped.
  • Skips the fetch when the picture URL is unchanged since last sync.

Setup: edit provider → enable Sync profile picture → (optional) set a custom Picture claim.


OIDC step-up for users (v2.5.7)

Users whose only factor is OIDC can satisfy the hardened self-service step-up by re-authenticating to a linked IdP in a popup.

  • The step-up modal shows a "🌐 Verify with ProviderName" button per linked IdP.
  • The popup opens with prompt=login so the IdP must actually re-authenticate (silent SSO is rejected).
  • The IdP-returned sub must match the user's stored SsoLink — signing into a different IdP account doesn't grant step-up.
  • The state token is single-use, 10-minute TTL, bound to the user + provider; the postMessage target is restricted to window.location.origin.

Hide built-in 2FA / Passkey login buttons (v2.5.7)

For OIDC-only deployments. Two independent toggles in Settings → Hardening:

  • Hide the "Sign in with Two-Factor Authentication" button.
  • Hide the "Sign in with passkey" button.

Configured OIDC provider buttons stay visible regardless.

/TwoFactorAuth/Login still works by direct URL even with both hidden — so you don't lock yourself out if the IdP becomes unreachable.


OIDC private / VPN / LAN endpoints (v2.5.7)

Point the plugin at an IdP on a private network (Tailscale, WireGuard, LAN-only Authentik/Authelia/PocketID). Without this, the SSRF guard rejects any discovery URL resolving to an RFC1918 / loopback / link-local address or using plain http.

  • Setting: per-provider, OIDC edit form → Allow private / VPN / LAN endpoints (Advanced, default off).
  • Granularity: per-provider. A public Google and a private Authentik coexist — Google keeps the strict guard, Authentik gets the bypass.

Trade-off: if a bypassed provider's discovery URL is tampered with, an attacker could pivot the plugin into internal services (cloud metadata at 169.254.169.254, internal admin APIs, the Docker socket). Only enable for IdPs you intentionally host on private networks where the network boundary is the security boundary.

Clone this wiki locally