Skip to content

Commit 358335f

Browse files
committed
feat: Jellyfin Remote Auth plugin — trusted header SSO with RBAC
Forward auth / remote user authentication for Jellyfin. Reverse proxy authenticates users and injects identity headers. Plugin reads headers, provisions users, applies role-based library access. Features: - Trusted header SSO (Authentik, Authelia, Traefik, Caddy) - Shared secret header with constant-time validation - Role-based library access control (group → permission mapping) - Auto user provisioning - Admin UI for configuration - CI: build + zizmor on PR, release pipeline on tag push - Manifest auto-update for plugin repository install
0 parents  commit 358335f

23 files changed

Lines changed: 2419 additions & 0 deletions

.github/workflows/build.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: '🏗️ Build Plugin'
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths-ignore:
8+
- '**/*.md'
9+
pull_request:
10+
branches:
11+
- main
12+
paths-ignore:
13+
- '**/*.md'
14+
workflow_dispatch:
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
build:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Checkout Repository
24+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25+
with:
26+
persist-credentials: false
27+
28+
- name: Setup .NET
29+
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
30+
with:
31+
dotnet-version: "9.0.x"
32+
33+
- name: Cache NuGet packages
34+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
35+
with:
36+
path: ~/.nuget/packages
37+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
38+
restore-keys: |
39+
${{ runner.os }}-nuget-
40+
41+
- name: Restore dependencies
42+
run: dotnet restore
43+
44+
- name: Build Jellyfin Plugin
45+
uses: oddstr13/jellyfin-plugin-repository-manager@9497a0a499416cc572ed2e07a391d9f943a37b4d # v1.1.1
46+
id: jprm
47+
with:
48+
dotnet-target: "net9.0"
49+
50+
- name: Upload Artifact
51+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
52+
with:
53+
name: build-artifact
54+
retention-days: 30
55+
if-no-files-found: error
56+
path: ${{ steps.jprm.outputs.artifact }}
57+
58+
zizmor:
59+
runs-on: ubuntu-latest
60+
permissions:
61+
contents: read
62+
steps:
63+
- name: Checkout Repository
64+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
65+
with:
66+
persist-credentials: false
67+
68+
- name: Run zizmor
69+
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
70+
with:
71+
inputs: "."
72+
online-audits: "false"
73+
advanced-security: "false"

.github/workflows/release.yml

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: '🚀 Release Plugin'
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout Repository
17+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
18+
with:
19+
persist-credentials: false
20+
21+
- name: Setup .NET
22+
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
23+
with:
24+
dotnet-version: "9.0.x"
25+
26+
- name: Restore dependencies
27+
run: dotnet restore
28+
29+
- name: Build Jellyfin Plugin
30+
uses: oddstr13/jellyfin-plugin-repository-manager@9497a0a499416cc572ed2e07a391d9f943a37b4d # v1.1.1
31+
id: jprm
32+
with:
33+
dotnet-target: "net9.0"
34+
35+
- name: Upload Artifact
36+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
37+
with:
38+
name: build-artifact
39+
path: ${{ steps.jprm.outputs.artifact }}
40+
41+
release:
42+
needs: [build]
43+
runs-on: ubuntu-latest
44+
if: startsWith(github.ref, 'refs/tags/v')
45+
permissions:
46+
contents: write
47+
steps:
48+
- name: Download Artifact
49+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
50+
with:
51+
name: build-artifact
52+
53+
- name: Generate checksums
54+
run: |
55+
for file in ./*.zip; do
56+
md5sum "${file##./}" >> "${file%.zip}.md5"
57+
sha256sum "${file##./}" >> "${file%.zip}.sha256"
58+
done
59+
60+
- name: Create GitHub Release
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
GH_REPO: ${{ github.repository }}
64+
TAG: ${{ github.ref_name }}
65+
run: |
66+
# Create release if it doesn't exist, or upload assets to existing one
67+
if gh release view "${TAG}" --repo "${GH_REPO}" > /dev/null 2>&1; then
68+
echo "Release ${TAG} already exists — uploading assets"
69+
gh release upload "${TAG}" ./* --repo "${GH_REPO}" --clobber
70+
else
71+
gh release create "${TAG}" ./* --generate-notes --repo "${GH_REPO}"
72+
fi
73+
74+
update-manifest:
75+
needs: [release]
76+
runs-on: ubuntu-latest
77+
if: startsWith(github.ref, 'refs/tags/v')
78+
permissions:
79+
contents: write
80+
steps:
81+
- name: Checkout Repository
82+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
83+
with:
84+
ref: main
85+
persist-credentials: false
86+
token: ${{ secrets.GITHUB_TOKEN }}
87+
88+
- name: Extract version from tag
89+
id: version
90+
env:
91+
TAG: ${{ github.ref_name }}
92+
run: |
93+
VERSION="${TAG#v}"
94+
# Convert semver (1.2.3) to 4-part (1.2.3.0) if needed
95+
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
96+
VERSION="${VERSION}.0"
97+
fi
98+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
99+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
100+
101+
- name: Update manifest.json
102+
env:
103+
VERSION: ${{ steps.version.outputs.version }}
104+
TAG: ${{ steps.version.outputs.tag }}
105+
REPO: ${{ github.repository }}
106+
run: |
107+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
108+
SOURCE_URL="https://github.com/${REPO}/releases/download/${TAG}/remote-auth_${VERSION}.zip"
109+
CHECKSUM=$(curl -sL "${SOURCE_URL}" | md5sum | cut -d' ' -f1)
110+
111+
# Use jq to update manifest.json — prepend new version entry
112+
jq --arg ver "$VERSION" \
113+
--arg ts "$TIMESTAMP" \
114+
--arg url "$SOURCE_URL" \
115+
--arg cs "$CHECKSUM" \
116+
--arg cl "Release ${TAG}" \
117+
'.[0].versions = [
118+
{
119+
"version": $ver,
120+
"changelog": $cl,
121+
"targetAbi": "10.11.0.0",
122+
"sourceUrl": $url,
123+
"checksum": $cs,
124+
"timestamp": $ts
125+
}
126+
] + .[0].versions' manifest.json > manifest_new.json
127+
mv manifest_new.json manifest.json
128+
129+
- name: Commit manifest
130+
env:
131+
TAG: ${{ steps.version.outputs.tag }}
132+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133+
REPO: ${{ github.repository }}
134+
run: |
135+
git config user.name "github-actions[bot]"
136+
git config user.email "github-actions[bot]@users.noreply.github.com"
137+
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
138+
git add manifest.json
139+
git commit -m "chore: update manifest.json for ${TAG}" || exit 0
140+
git push

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
bin/
2+
obj/
3+
*.user
4+
*.suo
5+
.vs/
6+
*.DotSettings.user
7+
TestResults/

.mise.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tools]
2+
zizmor = "1"

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
2+
WORKDIR /src
3+
COPY Jellyfin.Plugin.RemoteAuth/Jellyfin.Plugin.RemoteAuth.csproj Jellyfin.Plugin.RemoteAuth/
4+
RUN dotnet restore Jellyfin.Plugin.RemoteAuth/Jellyfin.Plugin.RemoteAuth.csproj
5+
COPY . .
6+
RUN dotnet publish Jellyfin.Plugin.RemoteAuth/Jellyfin.Plugin.RemoteAuth.csproj \
7+
-c Release -o /app/publish
8+
9+
FROM scratch AS artifact
10+
COPY --from=build /app/publish/*.dll /
11+
COPY --from=build /app/publish/meta.json /
12+
13+
FROM build AS package-build
14+
RUN apt-get update && apt-get install -y zip && rm -rf /var/lib/apt/lists/*
15+
RUN cd /app/publish && zip /remote-auth.zip *.dll meta.json
16+
17+
FROM scratch AS package
18+
COPY --from=package-build /remote-auth.zip /
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Collections.Generic;
2+
using Jellyfin.Plugin.RemoteAuth.Services;
3+
using MediaBrowser.Common.Api;
4+
using Microsoft.AspNetCore.Authorization;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace Jellyfin.Plugin.RemoteAuth.Api;
8+
9+
[ApiController]
10+
[Route("sso/RemoteAuth/Config")]
11+
[Authorize(Policy = Policies.RequiresElevation)]
12+
public class ConfigController : ControllerBase
13+
{
14+
private readonly RbacService _rbacService;
15+
16+
public ConfigController(RbacService rbacService)
17+
{
18+
_rbacService = rbacService;
19+
}
20+
21+
[HttpGet("Libraries")]
22+
public ActionResult<Dictionary<string, string>> GetLibraries()
23+
{
24+
return Ok(_rbacService.GetAvailableLibraries());
25+
}
26+
27+
[HttpGet("Status")]
28+
public ActionResult GetStatus()
29+
{
30+
var config = RemoteAuthPlugin.Instance?.Configuration;
31+
return Ok(new
32+
{
33+
PluginVersion = RemoteAuthPlugin.Instance?.Version?.ToString() ?? "unknown",
34+
Enabled = config?.Enabled ?? false,
35+
Configured = !string.IsNullOrWhiteSpace(config?.SecretHeaderValue),
36+
RoleMappingCount = config?.RoleMappings.Count ?? 0,
37+
AutoCreateUsers = config?.AutoCreateUsers ?? false,
38+
DefaultRoleName = config?.DefaultRoleName ?? string.Empty
39+
});
40+
}
41+
}

0 commit comments

Comments
 (0)