Use the same classes the login page puts on its own buttons so the SSO link picks up the theme styling, and note that copying a real button's markup does not work: a button cannot navigate without an href and DOMPurify removes onclick. Warn against copying btnQuick, which is the Quick Connect binding hook rather than styling. Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
6.5 KiB
Markdown
153 lines
6.5 KiB
Markdown
# Jellyfin OIDC Auth Plugin
|
|
|
|
Log in to Jellyfin with any OpenID Connect provider (Authelia, Authentik, Keycloak, Pocket ID, Google, ...).
|
|
|
|
## How it works
|
|
|
|
1. User opens `https://<jellyfin>/OidcAuth/login`.
|
|
2. Plugin redirects to your provider (authorization code flow + PKCE via `IdentityModel.OidcClient`).
|
|
3. Provider redirects back to `https://<jellyfin>/OidcAuth/callback`; the plugin exchanges the code, validates the tokens, and maps claims to a Jellyfin user (creating it if configured).
|
|
4. A small callback page creates a Jellyfin session (`ISessionManager.AuthenticateDirect`), writes the credentials into the web client's local storage, and redirects to `/web/`.
|
|
|
|
## Build
|
|
|
|
Requires the .NET 9 SDK and `pyyaml`. The script compiles the plugin and packages
|
|
it the way Jellyfin expects — the DLLs listed in `build.yaml` plus a generated
|
|
`meta.json` — into `dist/oidc-auth_<version>.zip`:
|
|
|
|
```bash
|
|
pip install pyyaml
|
|
python scripts/build_plugin.py
|
|
```
|
|
|
|
Pass `--version 1.0.4.0` to override the version in `build.yaml`.
|
|
|
|
## Release
|
|
|
|
Releases are cut by pushing an annotated tag; the tag message becomes the changelog:
|
|
|
|
```bash
|
|
git tag -a v1.0.4.0 -m "What changed in this release."
|
|
git push origin v1.0.4.0
|
|
```
|
|
|
|
`.gitea/workflows/release.yml` then builds the plugin at that tag, commits the zip
|
|
to `dist/` on `main` together with an updated `manifest.json`, and attaches the zip
|
|
to the Gitea release. Jellyfin picks the new version up from the repository URL below.
|
|
|
|
Pushing needs a token with write access. Gitea's built-in `GITHUB_TOKEN` is used by
|
|
default; if the repository does not grant it write access, add a PAT as the
|
|
`RELEASE_TOKEN` secret.
|
|
|
|
## Install via plugin repository (recommended)
|
|
|
|
1. In Jellyfin: Dashboard → Plugins → Repositories → **+**
|
|
2. Repository URL:
|
|
|
|
```
|
|
https://git.linxweiler.xyz/pascallinxweiler/jellyfinsso/raw/branch/main/manifest.json
|
|
```
|
|
|
|
3. Catalog → Authentication → **OIDC Auth** → Install → restart Jellyfin.
|
|
|
|
Updates show up in the catalog automatically when a new version is added to `manifest.json`.
|
|
|
|
## Install manually
|
|
|
|
Extract `dist/oidc-auth_<version>.zip` into a new folder in Jellyfin's plugin
|
|
directory (e.g. `config/plugins/OidcAuth/`), then restart Jellyfin. The zip holds:
|
|
|
|
- `Jellyfin.Plugin.OidcAuth.dll`
|
|
- `IdentityModel.OidcClient.dll`
|
|
- `IdentityModel.dll`
|
|
- `meta.json`
|
|
|
|
Requires Jellyfin 10.11.x.
|
|
|
|
## Configure
|
|
|
|
At your OIDC provider, create a **confidential web client** with redirect URI:
|
|
|
|
```
|
|
https://<jellyfin>/OidcAuth/callback
|
|
```
|
|
|
|
Then in Jellyfin: Dashboard → Plugins → OIDC Auth:
|
|
|
|
| Setting | Meaning |
|
|
|---|---|
|
|
| Issuer URL | Provider base URL; discovery doc must exist at `<issuer>/.well-known/openid-configuration` |
|
|
| Client ID / Secret | From your provider |
|
|
| Scopes | Default `openid profile email`; add `groups` if roles come via that scope |
|
|
| Username claim | Default `preferred_username`, falls back to `sub` |
|
|
| Role claim | Default `groups` |
|
|
| Allowed roles | CSV; empty = every authenticated user may log in |
|
|
| Admin roles | CSV; users with one of these roles are granted admin on SSO login. Missing role does **not** revoke admin unless "Also revoke admin" is enabled. Empty = plugin never touches the admin flag |
|
|
| Create users on first login | On by default; new users get access to all libraries |
|
|
| Set random password for auto-created users | On by default. Without it, Jellyfin accepts a **blank password** for these users from any client — convenient on a LAN-only server, dangerous on an exposed one |
|
|
| Disable strict endpoint validation | Enable for Google and other providers whose endpoints are on a different host than the issuer |
|
|
|
|
## Login button on the web login page
|
|
|
|
Jellyfin does not let plugins modify the login page. The login disclaimer is the
|
|
only injection point, and it needs a little CSS to look like a real button.
|
|
|
|
Dashboard → General → Branding → **Login disclaimer**:
|
|
|
|
```html
|
|
<a href="/OidcAuth/login" class="raised cancel block emby-button"><span>Sign in with SSO</span></a>
|
|
```
|
|
|
|
`cancel` gives the grey look of the Quick Connect button; swap it for
|
|
`button-submit` to match the blue Sign In button. Do not copy `btnQuick` off the
|
|
Quick Connect button — that class is what the login page binds Quick Connect to,
|
|
not styling.
|
|
|
|
Dashboard → General → Branding → **Custom CSS**:
|
|
|
|
```css
|
|
.disclaimerContainer {
|
|
display: block;
|
|
}
|
|
|
|
a.raised.emby-button {
|
|
padding: 0.9em 1em;
|
|
color: inherit !important;
|
|
text-decoration: none;
|
|
}
|
|
```
|
|
|
|
Both pieces are needed. Jellyfin renders the disclaimer through markdown-it and
|
|
then DOMPurify, which strips the `is="emby-linkbutton"` attribute that would
|
|
normally turn the anchor into a button component — so the styling has to come
|
|
from the classes instead. `.disclaimerContainer` is not a block by default,
|
|
which is what otherwise leaves the link shrink-wrapped and centred instead of
|
|
matching the width of the buttons above it.
|
|
|
|
It has to be an anchor. Copying the markup of a real login button does not work:
|
|
`<button>` cannot navigate without an `href`, and DOMPurify removes `onclick`
|
|
along with `is`, so the result renders correctly and does nothing when clicked.
|
|
|
|
Styling via the theme's own classes rather than inline styles keeps the button
|
|
correct when the theme changes, since `.raised` is defined per theme.
|
|
|
|
## TV and native clients
|
|
|
|
The plugin does not change or disable Jellyfin's normal password login, so TV apps
|
|
(Android TV, Roku, Swiftfin, Kodi, ...) keep working exactly as before. For users
|
|
that exist only through OIDC, three options:
|
|
|
|
1. **Quick Connect** (recommended): enable it in Dashboard → General. On the TV choose
|
|
Quick Connect, then approve the code from the web client (where you're logged in via OIDC).
|
|
2. **Set a password**: after the first OIDC login, the user sets a password in their
|
|
web profile and uses that on the TV.
|
|
3. **Blank password** (LAN-only setups): turn off "Set random password for auto-created
|
|
users" — TV login then works with username + empty password. Do **not** do this on a
|
|
server reachable from the internet.
|
|
|
|
## Notes / limitations
|
|
|
|
- The OIDC browser flow itself works for the **web client** (and anything that can open a browser and reuse the resulting session token). Native clients that only show the username/password form can't use this flow directly — see the TV section above.
|
|
- Behind a reverse proxy, make sure Jellyfin sees the external scheme/host (`X-Forwarded-Proto`, `X-Forwarded-Host` — Jellyfin's "Known proxies" setting), otherwise the computed redirect URI won't match the one registered at the provider.
|
|
- Password login stays enabled; this plugin adds an additional login path.
|