A Jellyfin plugin for trusted header SSO (forward auth / remote user authentication).
Your reverse proxy (Authentik, Authelia, Traefik ForwardAuth, Caddy, nginx auth_request…) authenticates the user and injects identity headers. This plugin reads those headers, provisions the Jellyfin user, applies role-based library access, and issues a session.
Browser Reverse Proxy Jellyfin Plugin
| | |
|-- request ------->| |
| |-- forward auth check ->|IdP
| |<-- 200 + headers ------|
| | |
| |-- GET /sso/RemoteAuth/Login
| | X-Remote-Auth-Secret: <secret>
| | X-Remote-Auth-User: alice
| | X-Remote-Auth-Groups: jellyfin-users|jellyfin-4k
| | |
| |<-- HTML (stores token, redirects to /)
|<-- redirect / ----| |
|-- authenticated --> |
- User hits the Jellyfin login page (or a protected URL).
- Reverse proxy intercepts, authenticates via IdP (OIDC, LDAP, etc.).
- Proxy makes a sub-request to
/sso/RemoteAuth/Loginwith the secret + identity headers. - Plugin validates the secret, syncs the user, applies RBAC, and returns a small HTML page.
- The HTML stores the Jellyfin session token in
localStorageand redirects to/.
This plugin uses two layers of security. Both are required.
Every request to /sso/RemoteAuth/Login must include the configured secret header. Requests without the correct value are rejected with 401. This provides defense-in-depth if something bypasses the network layer.
The comparison is done in constant time (CryptographicOperations.FixedTimeEquals) to prevent timing attacks. Use a long random string (32+ bytes) as the secret.
The secret alone is not sufficient. An attacker who can reach the endpoint directly and brute-force or guess the secret could authenticate as any user.
You must ensure that only your reverse proxy can reach /sso/RemoteAuth/Login.
Docker / Docker Compose
Put Jellyfin on an internal network. Only the proxy container should be on that network.
networks:
proxy: # proxy → Jellyfin
internal: false
internal: # Jellyfin-only services
internal: true
services:
proxy:
networks: [proxy]
jellyfin:
networks: [proxy, internal]
# Do NOT publish port 8096 directlyKubernetes — NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: jellyfin-remoteauth-ingress
namespace: jellyfin
spec:
podSelector:
matchLabels:
app: jellyfin
policyTypes: [Ingress]
ingress:
# Allow Traefik/proxy to reach the SSO endpoint
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
podSelector:
matchLabels:
app: traefik
ports:
- port: 8096
# Allow normal Jellyfin traffic from ingress only
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefikBoth layers (secret + network isolation) should be used together.
- In Jellyfin, go to Admin Dashboard → Plugins → Repositories
- Add a new repository:
- Name:
Remote Auth - URL:
https://raw.githubusercontent.com/BlackDark/jellyfin-plugin-remoteauth/main/manifest.json
- Name:
- Go to Catalog, find "Remote Auth" under Authentication, and install it
- Restart Jellyfin
The manifest is automatically updated on each release.
- Download the latest
.zipfrom Releases - Extract to your Jellyfin plugins directory:
# Linux unzip remote-auth_*.zip -d /var/lib/jellyfin/plugins/RemoteAuth/ # Docker — mount or copy into container unzip remote-auth_*.zip -d /config/plugins/RemoteAuth/
- Restart Jellyfin
# With .NET 9 SDK
make build
# Copy to Jellyfin plugins directory
sudo cp dist/*.dll dist/meta.json /var/lib/jellyfin/plugins/RemoteAuth/
sudo systemctl restart jellyfinmake docker-build
sudo cp dist/*.dll dist/meta.json /var/lib/jellyfin/plugins/RemoteAuth/
sudo systemctl restart jellyfinGo to Admin Dashboard → Plugins → Remote Auth.
| Field | Default | Description |
|---|---|---|
| Enable Remote Auth | true | Master on/off switch |
| Secret Header Name | X-Remote-Auth-Secret |
Name of the shared secret header |
| Secret Header Value | (empty) | The shared secret — plugin refuses requests if blank |
| Username Header | X-Remote-Auth-User |
Header carrying the username |
| Email Header | X-Remote-Auth-Email |
Optional email header |
| Display Name Header | X-Remote-Auth-Name |
Optional display name header |
| Groups Header | X-Remote-Auth-Groups |
Header with pipe-delimited group list |
| Groups Delimiter | | |
Delimiter used in the groups header |
| Admin Group | (empty) | Shortcut: users in this group always get admin |
| Auto-create Users | true | Create Jellyfin accounts on first login |
| Default Role | (empty) | Fallback role mapping when no groups match |
Map IdP group names to Jellyfin permissions. Multiple matched groups are merged (union semantics — most permissive wins).
Plugin settings are stored as XML on the Jellyfin server (not in this repository). The admin UI reads and writes this file via Jellyfin's plugin system.
| Install | Path |
|---|---|
| Linux (package) | /var/lib/jellyfin/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xml |
| Docker | /config/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xml |
| macOS | ~/Library/Application Support/jellyfin/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xml |
| Windows (tray) | %ProgramData%\Jellyfin\Server\plugins\configurations\Jellyfin.Plugin.RemoteAuth.xml |
The file is created when you first save settings in the dashboard. Until then, defaults from PluginConfiguration.cs apply.
Backup before upgrades:
# Linux
sudo cp /var/lib/jellyfin/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xml{,.bak}
# Docker
docker cp jellyfin:/config/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xml ./remote-auth-config.xml.bakJellyfin retains plugin settings across plugin updates as long as this file is not deleted.
Apply config automatically (GitOps, Ansible, cloud-init, etc.):
- Configure once in the UI (or copy from an existing server).
- Copy the XML to your target path before or after plugin install.
- Restart Jellyfin — file edits do not hot-reload.
# Example: deploy pre-built config on Docker start
install -d -m 755 /config/plugins/configurations
install -m 600 remote-auth-config.xml /config/plugins/configurations/Jellyfin.Plugin.RemoteAuth.xmlExample structure (values will match your setup):
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration>
<Enabled>true</Enabled>
<SecretHeaderName>X-Remote-Auth-Secret</SecretHeaderName>
<SecretHeaderValue>your-shared-secret</SecretHeaderValue>
<UserHeader>X-Remote-Auth-User</UserHeader>
<EmailHeader>X-Remote-Auth-Email</EmailHeader>
<DisplayNameHeader>X-Remote-Auth-Name</DisplayNameHeader>
<GroupsHeader>X-Remote-Auth-Groups</GroupsHeader>
<GroupsDelimiter>|</GroupsDelimiter>
<AdminGroup>jellyfin-admins</AdminGroup>
<AutoCreateUsers>true</AutoCreateUsers>
<DefaultRoleName>viewer</DefaultRoleName>
<RoleMappings>
<RoleMapping>
<RoleName>viewer</RoleName>
<IsAdmin>false</IsAdmin>
<EnableAllLibraries>true</EnableAllLibraries>
<LibraryIds />
<LibraryNames />
<EnableMediaPlayback>true</EnableMediaPlayback>
<EnableRemoteAccess>true</EnableRemoteAccess>
<EnableTranscoding>true</EnableTranscoding>
<Priority>0</Priority>
</RoleMapping>
</RoleMappings>
</PluginConfiguration>To resolve library GUIDs for LibraryIds, use Admin → Plugins → Remote Auth or GET /sso/RemoteAuth/Config/Libraries while logged in as admin.
Note:
SecretHeaderValueis stored in plain text. Restrict file permissions (chmod 600) and treat the XML like a secret.
Authentik outpost handles forward auth. Configure the provider's additional headers to inject user info.
In Authentik: go to Providers → Your Provider → Advanced → Additional scopes/headers and add:
X-Remote-Auth-User: %(username)s
X-Remote-Auth-Name: %(name)s
X-Remote-Auth-Groups: %(groups | join("|"))s
Traefik middleware:
# traefik/config/middlewares.yml
http:
middlewares:
authentik:
forwardAuth:
address: "http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik"
trustForwardHeader: true
authResponseHeaders:
- X-Remote-Auth-User
- X-Remote-Auth-Name
- X-Remote-Auth-GroupsJellyfin service label:
labels:
- "traefik.http.routers.jellyfin.middlewares=authentik@file"Proxy must also forward the secret. Add to the outpost or middleware chain:
# Add the secret as a request header before forwarding to Jellyfin
- "traefik.http.middlewares.add-ra-secret.headers.customrequestheaders.X-Remote-Auth-Secret=your-long-random-secret"Set the same value in the plugin's Secret Header Value field.
jellyfin.example.com {
forward_auth authentik:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Remote-Auth-User X-Remote-Auth-Name X-Remote-Auth-Groups
}
# Inject the secret
header X-Remote-Auth-Secret "your-long-random-secret"
reverse_proxy jellyfin:8096
}# authelia config
access_control:
rules:
- domain: jellyfin.example.com
policy: one_factorTraefik / nginx forwards Remote-User, Remote-Groups headers. Map them in the plugin's header config fields.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jellyfin
annotations:
traefik.ingress.kubernetes.io/router.middlewares: "traefik-authentik@kubernetescrd"
spec:
rules:
- host: jellyfin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jellyfin
port:
number: 8096
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authentik
namespace: traefik
spec:
forwardAuth:
address: http://authentik-proxy.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- X-Remote-Auth-User
- X-Remote-Auth-Name
- X-Remote-Auth-GroupsWhen a user matches multiple role mappings (because they're in multiple groups), permissions are merged with union semantics:
- Boolean permissions:
trueif any matched mapping has it enabled - Libraries: union of all matched mappings' library sets
EnableAllLibraries:trueif any mapping enables itMaxParentalRating: highest value across all matched mappings
If none of the user's groups match any role mapping, the Default Role (configured in General tab) is used as a fallback. Leave blank to deny access to unmatched users.
If Admin Group is set and a user is a member of that group, they receive IsAdministrator = true regardless of role mappings.
Smart TV and mobile clients authenticate via /Users/AuthenticateByName (standard Jellyfin API) — they are not affected by this plugin. Remote Auth only manages the web-based SSO flow via /sso/RemoteAuth/Login.
Users managed by this plugin have password login disabled. If you need to allow TV clients for SSO users, you can either:
- Set up the Jellyfin TV app with the session token from a web login (token sharing), or
- Leave standard auth enabled for a separate TV-only account.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /sso/RemoteAuth/Login |
Secret header | Header-based login (called by proxy) |
| GET | /sso/RemoteAuth/Config/Libraries |
Admin | List available libraries |
| GET | /sso/RemoteAuth/Config/Status |
Admin | Plugin status |
Releases are automated with release-please from Conventional Commits on main.
| Commit prefix | Version bump |
|---|---|
fix: |
patch (1.0.1 → 1.0.2) |
feat: |
minor (1.0.1 → 1.1.0) |
feat!: / fix!: / BREAKING CHANGE: |
major (1.0.1 → 2.0.0) |
Flow:
- Push conventional commits to
main - release-please opens/updates a Release PR (version +
CHANGELOG.md) - Merge the Release PR → tag
v1.0.2+ GitHub release created release.ymlbuilds the plugin zip, uploads assets, updatesmanifest.json
Git tags use 3-part semver (v1.0.2). Jellyfin manifest uses 4-part plugin versions (1.0.2.0) — the release workflow pads automatically.
make validate-manifest # verify manifest URLs/checksums before shipping- .NET 9.0 SDK or Docker
# SDK
make build
# Docker only
make docker-build
# Installable zip
make packageJellyfin.Plugin.RemoteAuth/
RemoteAuthPlugin.cs # Plugin entry point
Configuration/
PluginConfiguration.cs # Config DTOs
configPage.html # Admin UI (embedded)
remoteauth.js # Admin UI JS (embedded)
Api/
RemoteAuthController.cs # /sso/RemoteAuth/Login
ConfigController.cs # Admin config API
Auth/
RemoteAuthProvider.cs # Blocks password login for managed users
Services/
RbacService.cs # Group → permission mapping engine
UserSyncService.cs # User provisioning and sync
ServiceRegistrator.cs # DI registration
GPLv3 (required by linking against Jellyfin's GPLv3 libraries)