mirror of
https://github.com/9p4/jellyfin-plugin-sso.git
synced 2026-09-19 13:12:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1e157bac2 | ||
|
|
83deeb253c | ||
|
|
ca187a7b5e | ||
|
|
05213dc1ee | ||
|
|
f7a0c94452 | ||
|
|
475048a32e | ||
|
|
781e07b2ce | ||
|
|
fed7e764a8 | ||
|
|
f0def6fd8d | ||
|
|
8e128932c4 | ||
|
|
5b3d70d328 | ||
|
|
be26670e1f | ||
|
|
547eabf55f | ||
|
|
48d75325b5 | ||
|
|
cdce0e583d | ||
|
|
0e897f922f | ||
|
|
afbab1073e | ||
|
|
87425aae36 | ||
|
|
9be9a1fed8 | ||
|
|
3b47851131 | ||
|
|
337ea0ba04 | ||
|
|
c37e3e3a71 | ||
|
|
8f86ce5101 | ||
|
|
d2c77db404 |
@@ -22,6 +22,8 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Project archived because I'm tired of working on this after all the years.
|
||||
|
||||
This plugin allows users to sign in through an SSO provider (such as Google, Microsoft, or your own provider). This enables one-click signin.
|
||||
|
||||
https://user-images.githubusercontent.com/17993169/149681516-f93b43f5-fa5c-4c1f-a909-e5414878a864.mp4
|
||||
@@ -46,11 +48,13 @@ This is 100% alpha software! PRs are welcome to improve the code.
|
||||
- authentik
|
||||
- Keycloak
|
||||
- OIDC & SAML
|
||||
- Pocket ID
|
||||
- Kanidm
|
||||
- Google OpenID: Works, but usernames are all numeric
|
||||
|
||||
## Supported Protocols
|
||||
|
||||
- [OpenID](https://openid.net/what-is-openid/)
|
||||
- [OpenID](https://openid.net/developers/how-connect-works/)
|
||||
- [SAML](https://www.cloudflare.com/learning/access-management/what-is-saml/)
|
||||
|
||||
## Security
|
||||
|
||||
+138
-28
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -42,28 +43,34 @@ public class SSOController : ControllerBase
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly ILogger<SSOController> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SSOController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SSOController}"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="cryptoProvider">Instance of the <see cref="ICryptoProvider"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public SSOController(
|
||||
ILogger<SSOController> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager,
|
||||
IAuthorizationContext authContext,
|
||||
ICryptoProvider cryptoProvider,
|
||||
IProviderManager providerManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
@@ -71,8 +78,10 @@ public class SSOController : ControllerBase
|
||||
_authContext = authContext;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
_providerManager = providerManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger.LogInformation("SSO Controller initialized");
|
||||
}
|
||||
|
||||
@@ -101,6 +110,16 @@ public class SSOController : ControllerBase
|
||||
|
||||
if (config.Enabled)
|
||||
{
|
||||
if (string.IsNullOrEmpty(state))
|
||||
{
|
||||
return BadRequest("Missing state");
|
||||
}
|
||||
|
||||
if (!StateManager.TryGetValue(state, out var timedState))
|
||||
{
|
||||
return BadRequest("Invalid or expired state");
|
||||
}
|
||||
|
||||
var scopes = config.OidScopes == null ? new string[2] : config.OidScopes;
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
@@ -110,6 +129,17 @@ public class SSOController : ControllerBase
|
||||
RedirectUri = GetRequestBase(config.SchemeOverride, config.PortOverride) + $"/sso/OID/{(Request.Path.Value.Contains("/start/", StringComparison.InvariantCultureIgnoreCase) ? "redirect" : "r")}/" + provider,
|
||||
Scope = string.Join(" ", scopes.Prepend("openid profile")),
|
||||
DisablePushedAuthorization = config.DisablePushedAuthorization,
|
||||
LoggerFactory = _loggerFactory,
|
||||
LoadProfile = !config.DoNotLoadProfile,
|
||||
HttpClientFactory = o =>
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||
string version = fvi.FileVersion;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||
return client;
|
||||
}
|
||||
};
|
||||
var oidEndpointUri = new Uri(config.OidEndpoint?.Trim());
|
||||
options.Policy.Discovery.AdditionalEndpointBaseAddresses.Add(oidEndpointUri.GetLeftPart(UriPartial.Authority));
|
||||
@@ -117,7 +147,7 @@ public class SSOController : ControllerBase
|
||||
options.Policy.Discovery.RequireHttps = !config.DisableHttps;
|
||||
options.Policy.Discovery.ValidateIssuerName = !config.DoNotValidateIssuerName;
|
||||
var oidcClient = new OidcClient(options);
|
||||
var currentState = StateManager[state].State;
|
||||
var currentState = timedState.State;
|
||||
var result = await oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).ConfigureAwait(false);
|
||||
|
||||
if (result.IsError)
|
||||
@@ -127,19 +157,19 @@ public class SSOController : ControllerBase
|
||||
|
||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||
{
|
||||
StateManager[state].Folders = new List<string>(config.EnabledFolders);
|
||||
timedState.Folders = new List<string>(config.EnabledFolders);
|
||||
}
|
||||
else
|
||||
{
|
||||
StateManager[state].Folders = new List<string>();
|
||||
timedState.Folders = new List<string>();
|
||||
}
|
||||
|
||||
StateManager[state].EnableLiveTv = config.EnableLiveTv;
|
||||
StateManager[state].EnableLiveTvManagement = config.EnableLiveTvManagement;
|
||||
timedState.EnableLiveTv = config.EnableLiveTv;
|
||||
timedState.EnableLiveTvManagement = config.EnableLiveTvManagement;
|
||||
|
||||
if (config.AvatarUrlFormat is not null)
|
||||
{
|
||||
StateManager[state].AvatarURL = result.User.Claims.Aggregate(
|
||||
timedState.AvatarURL = result.User.Claims.Aggregate(
|
||||
config.AvatarUrlFormat,
|
||||
(s, claim) => s.Contains($"@{{{claim.Type}}}") ? s.Replace($"@{{{claim.Type}}}", claim.Value) : s);
|
||||
}
|
||||
@@ -148,10 +178,10 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (claim.Type == (config.DefaultUsernameClaim?.Trim() ?? "preferred_username"))
|
||||
{
|
||||
StateManager[state].Username = claim.Value;
|
||||
timedState.Username = claim.Value;
|
||||
if (config.Roles == null || config.Roles.Length == 0)
|
||||
{
|
||||
StateManager[state].Valid = true;
|
||||
timedState.Valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +207,40 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
// We recursively traverse through the JSON data for the roles and parse it
|
||||
var json = JsonConvert.DeserializeObject<IDictionary<string, object>>(claim.Value);
|
||||
for (int i = 1; i < segments.Length - 1; i++)
|
||||
if (json is null)
|
||||
{
|
||||
var segment = segments[i];
|
||||
json = (json[segment] as JObject).ToObject<IDictionary<string, object>>();
|
||||
roles = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
bool missingSegment = false;
|
||||
for (int i = 1; i < segments.Length - 1; i++)
|
||||
{
|
||||
var segment = segments[i];
|
||||
if (!json.TryGetValue(segment, out var nextToken) || nextToken is not JObject nextObject)
|
||||
{
|
||||
missingSegment = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// The final step is to take the JSON and turn it from a dictionary into a string
|
||||
roles = (json[segments[^1]] as JArray).ToObject<List<string>>();
|
||||
json = nextObject.ToObject<IDictionary<string, object>>();
|
||||
if (json is null)
|
||||
{
|
||||
missingSegment = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingSegment || !json.TryGetValue(segments[^1], out var rolesToken) || rolesToken is not JArray rolesArray)
|
||||
{
|
||||
roles = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The final step is to take the JSON and turn it from a dictionary into a string
|
||||
roles = rolesArray.ToObject<List<string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string role in roles)
|
||||
@@ -196,7 +252,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (role.Equals(validRoles))
|
||||
{
|
||||
StateManager[state].Valid = true;
|
||||
timedState.Valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +264,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (role.Equals(validAdminRoles))
|
||||
{
|
||||
StateManager[state].Admin = true;
|
||||
timedState.Admin = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +276,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (role.Equals(folderRoleMap.Role?.Trim()))
|
||||
{
|
||||
StateManager[state].Folders.AddRange(folderRoleMap.Folders);
|
||||
timedState.Folders.AddRange(folderRoleMap.Folders);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +290,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (role.Equals(validLiveTvRoles))
|
||||
{
|
||||
StateManager[state].EnableLiveTv = true;
|
||||
timedState.EnableLiveTv = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +302,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
if (role.Equals(validLiveTvManagementRoles))
|
||||
{
|
||||
StateManager[state].EnableLiveTvManagement = true;
|
||||
timedState.EnableLiveTvManagement = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,24 +313,24 @@ public class SSOController : ControllerBase
|
||||
}
|
||||
|
||||
// If the provider doesn't support the preferred username claim, then use the sub claim
|
||||
if (!StateManager[state].Valid)
|
||||
if (!timedState.Valid)
|
||||
{
|
||||
foreach (var claim in result.User.Claims)
|
||||
{
|
||||
if (claim.Type == "sub")
|
||||
{
|
||||
StateManager[state].Username = claim.Value;
|
||||
timedState.Username = claim.Value;
|
||||
if (config.Roles.Length == 0)
|
||||
{
|
||||
StateManager[state].Valid = true;
|
||||
timedState.Valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isLinking = StateManager[state].IsLinking;
|
||||
bool isLinking = timedState.IsLinking;
|
||||
|
||||
if (StateManager[state].Valid)
|
||||
if (timedState.Valid)
|
||||
{
|
||||
_logger.LogInformation($"Is request linking: {isLinking}");
|
||||
return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(config.SchemeOverride, config.PortOverride), mode: "OID", isLinking: isLinking), MediaTypeNames.Text.Html);
|
||||
@@ -283,7 +339,7 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"OpenID user {Username} has one or more incorrect role claims: {@Claims}. Expected any one of: {@ExpectedClaims}",
|
||||
StateManager[state].Username,
|
||||
timedState.Username,
|
||||
result.User.Claims.Select(o => new { o.Type, o.Value }),
|
||||
config.Roles);
|
||||
|
||||
@@ -335,6 +391,18 @@ public class SSOController : ControllerBase
|
||||
RedirectUri = redirectUri,
|
||||
Scope = string.Join(" ", config.OidScopes.Prepend("openid profile")),
|
||||
DisablePushedAuthorization = config.DisablePushedAuthorization,
|
||||
LoggerFactory = _loggerFactory,
|
||||
LoadProfile = !config.DoNotLoadProfile,
|
||||
HttpClientFactory = o =>
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||
string version = fvi.FileVersion;
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||
return client;
|
||||
}
|
||||
};
|
||||
var oidEndpointUri = new Uri(config.OidEndpoint?.Trim());
|
||||
options.Policy.Discovery.AdditionalEndpointBaseAddresses.Add(oidEndpointUri.GetLeftPart(UriPartial.Authority));
|
||||
@@ -457,8 +525,19 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("oid", provider, kvp.Value.Username);
|
||||
|
||||
var authenticationResult = await Authenticate(userId, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), kvp.Value.EnableLiveTv, kvp.Value.EnableLiveTvManagement, response, config.DefaultProvider?.Trim(), kvp.Value.AvatarURL)
|
||||
var authenticationResult = await Authenticate(
|
||||
userId,
|
||||
kvp.Value.Admin,
|
||||
config.EnableAuthorization,
|
||||
config.EnableAllFolders,
|
||||
kvp.Value.Folders.ToArray(),
|
||||
kvp.Value.EnableLiveTv,
|
||||
kvp.Value.EnableLiveTvManagement,
|
||||
response,
|
||||
config.DefaultProvider?.Trim(),
|
||||
kvp.Value.AvatarURL)
|
||||
.ConfigureAwait(false);
|
||||
StateManager.Remove(kvp.Key);
|
||||
return Ok(authenticationResult);
|
||||
}
|
||||
}
|
||||
@@ -499,6 +578,11 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
|
||||
|
||||
if (!samlResponse.IsValid())
|
||||
{
|
||||
return Problem("Invalid SAML signature");
|
||||
}
|
||||
|
||||
bool valid = false;
|
||||
|
||||
// If no roles are configured, don't use RBAC
|
||||
@@ -657,6 +741,12 @@ public class SSOController : ControllerBase
|
||||
bool liveTv = config.EnableLiveTv;
|
||||
bool liveTvManagement = config.EnableLiveTvManagement;
|
||||
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
||||
|
||||
if (!samlResponse.IsValid())
|
||||
{
|
||||
return Problem("Invalid SAML signature");
|
||||
}
|
||||
|
||||
List<string> folders;
|
||||
if (!config.EnableFolderRoles && config.EnabledFolders != null)
|
||||
{
|
||||
@@ -722,7 +812,17 @@ public class SSOController : ControllerBase
|
||||
|
||||
Guid userId = await CreateCanonicalLinkAndUserIfNotExist("saml", provider, samlResponse.GetNameID());
|
||||
|
||||
var authenticationResult = await Authenticate(userId, isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), liveTv, liveTvManagement, response, config.DefaultProvider?.Trim(), null)
|
||||
var authenticationResult = await Authenticate(
|
||||
userId,
|
||||
isAdmin,
|
||||
config.EnableAuthorization,
|
||||
config.EnableAllFolders,
|
||||
folders.ToArray(),
|
||||
liveTv,
|
||||
liveTvManagement,
|
||||
response,
|
||||
config.DefaultProvider?.Trim(),
|
||||
null)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(authenticationResult);
|
||||
}
|
||||
@@ -985,7 +1085,11 @@ public class SSOController : ControllerBase
|
||||
}
|
||||
|
||||
var samlResponse = new Response(config.SamlCertificate, response.Data);
|
||||
// TODO: Does saml response require further validation?
|
||||
|
||||
if (!samlResponse.IsValid())
|
||||
{
|
||||
return Problem("Invalid SAML signature");
|
||||
}
|
||||
|
||||
string providerUserId = samlResponse.GetNameID();
|
||||
|
||||
@@ -1095,7 +1199,13 @@ public class SSOController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
using var client = _httpClientFactory.CreateClient();
|
||||
|
||||
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
System.Diagnostics.FileVersionInfo fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||
string version = fvi.FileVersion;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Jellyfin-Plugin-SSO-Auth +{version} (https://github.com/9p4/jellyfin-plugin-sso)");
|
||||
|
||||
var avatarResponse = await client.GetAsync(avatarUrl);
|
||||
|
||||
if (!avatarResponse.Content.Headers.TryGetValues("content-type", out var contentTypeList))
|
||||
|
||||
@@ -327,6 +327,11 @@ public class OidConfig
|
||||
/// Gets or sets a value indicating whether the OpenID issuer name is validated.
|
||||
/// </summary>
|
||||
public bool DoNotValidateIssuerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the UserInfo endpoint is used to get profile data.
|
||||
/// </summary>
|
||||
public bool DoNotLoadProfile { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -480,7 +480,9 @@
|
||||
list of strings from the OIDC server.
|
||||
<br />
|
||||
For Keycloak, it is <code>realm_access.roles</code> by
|
||||
default.
|
||||
default for realm roles. For client roles, it is
|
||||
<code>resource_access.>clientId<.roles</code>
|
||||
(e.g. resource_access.jellyfin.roles)
|
||||
<br />
|
||||
For Authelia, it is <code>groups</code>
|
||||
</div>
|
||||
@@ -637,6 +639,23 @@
|
||||
<span>Do Not Validate OpenID Issuer Name (Insecure)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="checkboxContainer checkboxContainer-withDescription"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
is="emby-checkbox"
|
||||
id="DoNotLoadProfile"
|
||||
name="DoNotLoadProfile"
|
||||
type="checkbox"
|
||||
class="sso-toggle"
|
||||
/>
|
||||
<span>Do Not Load Profile Information</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">
|
||||
May be required for Cloudflare OpenID
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="RoleClaim"
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
|
||||
<AssemblyVersion>4.0.0.0</AssemblyVersion>
|
||||
<FileVersion>4.0.0.0</FileVersion>
|
||||
<AssemblyVersion>4.0.0.4</AssemblyVersion>
|
||||
<FileVersion>4.0.0.4</FileVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
name: "SSO Authentication"
|
||||
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
|
||||
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
|
||||
version: "4.0.0.0"
|
||||
version: "4.0.0.4"
|
||||
targetAbi: "10.11.0.0"
|
||||
framework: "net9.0"
|
||||
owner: "9p4"
|
||||
@@ -15,6 +15,7 @@ artifacts:
|
||||
- "Duende.IdentityModel.OidcClient.dll"
|
||||
- "Duende.IdentityModel.dll"
|
||||
changelog: |
|
||||
4.0.0.4: Fix security issue in SAML
|
||||
4.0.0.0: Jellyfin 10.11
|
||||
3.5.3.0: Allow for OID-provided avatars, various bugfixes and workarounds
|
||||
3.5.2.4: Updates for Jellyfin 10.9
|
||||
|
||||
+82
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
This plugin has been tested to work against various providers, though not all providers provide support for all of this plugins' features.
|
||||
|
||||
❗ Before you proceed, make sure you have another admin account if you are going to link SSO provider to the only admin account on the server, permission might get overwritten (see [#212](https://github.com/9p4/jellyfin-plugin-sso/issues/212)).
|
||||
|
||||
## TOC / Tested Providers:
|
||||
|
||||
This section is broken into providers that support Role-Based Access Control (RBAC), and those that do not
|
||||
@@ -10,8 +12,9 @@ This section is broken into providers that support Role-Based Access Control (RB
|
||||
|
||||
- ✅ [Authelia](#authelia)
|
||||
- ✅ [authentik](#authentik)
|
||||
- [✅ Keycloak](#keycloak-oidc)
|
||||
- ✅ [Keycloak](#keycloak-oidc)
|
||||
- Both [OIDC](#keycloak-oidc) & [SAML](#keycloak-saml)
|
||||
- ✅ [Pocket ID](#pocket-id)
|
||||
|
||||
### No RBAC Support
|
||||
|
||||
@@ -88,6 +91,7 @@ authelia:
|
||||
OidSecret: <redacted>
|
||||
RoleClaim: groups
|
||||
OidScopes: ["groups"]
|
||||
DisablePushedAuthorization: true
|
||||
```
|
||||
|
||||
## authentik
|
||||
@@ -222,3 +226,80 @@ keycloak:
|
||||
SamlClientId: <same-as-in-keycloak>
|
||||
SamlCertificate: <copied-from-xml-file>
|
||||
```
|
||||
|
||||
## Pocket ID
|
||||
|
||||
A simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.
|
||||
|
||||
### Pocket ID Config
|
||||
|
||||
1. Login to you Pocket ID admin account
|
||||
1. Go to `Administration -> OCID Clients`
|
||||
1. Click `Add OCID Client`
|
||||
1. Give the client a name e.g. `Jellyfin`
|
||||
1. Set the `Clent Launch URL` to your Jellyfin endpoint
|
||||
1. Set the callbak url to `https://jellyfin.example.com/sso/OID/redirect/pocketid`. The `pocketid` part must match the `Name of OpenID Provider` in the Jellyfin SSO provider
|
||||
1. (optional) Enable PKCE if Jellyfin is an https endpoint
|
||||
1. (optional) Set a logo
|
||||
1. (optional) Set `Allowed User Groups`
|
||||
|
||||
### Jellyfin's Config
|
||||
|
||||
```yaml
|
||||
pocketid:
|
||||
OidEndpoint: https://pocketid.example.com/.well-known/openid-configuration
|
||||
OidClientId: <pocket-id-client-id>
|
||||
OidSecret: <pocket-id-secret>
|
||||
EnableAuthorization: true # (optional) If you want Jellyfin to read group permissions from pocket id
|
||||
RoleClaim: groups # (optional) If you want Jellyfin to be able to read group assignments from pocket id
|
||||
AdminRoles: admin # (optional) The pocket id group which will give a user Jellyfin admin privilges
|
||||
Roles: users # (optional) The pocket id group which will give a user Jellyfin access
|
||||
AvatarUrlFormat: @{picture} # (optional) This will pull each users pocket id photo into Jellyfin
|
||||
```
|
||||
|
||||
## Kanidm
|
||||
|
||||
Kanidm is a modern and simple identity management platform written in rust.
|
||||
|
||||
### Kanidm Config
|
||||
|
||||
```shell
|
||||
kanidm system oauth2 create jellyfin "Jellyfin" https://jellyfin.example.com/
|
||||
|
||||
# Set this to drop the trailing @idm.example.com in usernames
|
||||
kanidm system oauth2 prefer-short-username jellyfin
|
||||
|
||||
kanidm system oauth2 add-redirect-url jellyfin https://jellyfin.example.com/sso/OID/redirect/kanidm
|
||||
kanidm system oauth2 add-redirect-url jellyfin https://jellyfin.example.com/sso/OID/r/kanidm
|
||||
|
||||
# Optionally setup groups for Jellyfin
|
||||
kanidm group create jellyfin_admins
|
||||
kanidm group create jellyfin_users
|
||||
|
||||
kanidm system oauth2 update-scope-map jellyfin jellyfin_admins openid profile groups
|
||||
kanidm system oauth2 update-scope-map jellyfin jellyfin_users openid profile groups
|
||||
```
|
||||
|
||||
Get the secret used in the Jellyfin config with `kanidm system oauth2 show-basic-secret jellyfin`.
|
||||
|
||||
### Jellyfin's Config
|
||||
|
||||
```yaml
|
||||
kanidm:
|
||||
OidEndpoint: https://idm.example.com/oauth2/openid/jellyfin/
|
||||
OidClientId: jellyfin
|
||||
OidSecret: <kanidm-secret>
|
||||
# (optional) If you want Jellyfin to read group permissions from kanidm
|
||||
EnableAuthorization: true
|
||||
OidScopes:
|
||||
- groups
|
||||
RoleClaim: groups
|
||||
AdminsRoles:
|
||||
- [email protected]
|
||||
Roles:
|
||||
- [email protected]
|
||||
# If in your setup admin accounts aren't members of the users group you need to add the admins group to roles as well
|
||||
- [email protected]
|
||||
# (optional) If you want the name attribute instead of the spn attribute as username
|
||||
DefaultUsernameClaim: preferred_username
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user