feat: initial Jellyfin OIDC auth plugin
OpenID Connect SSO for Jellyfin 10.10 (net8.0). Authorization code flow with PKCE via IdentityModel.OidcClient, automatic user creation with random password, role based admin/access mapping, plugin repo manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.OidcAuth.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin configuration for OIDC authentication.
|
||||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the OIDC issuer URL (authority). Discovery document is fetched from
|
||||
/// {issuer}/.well-known/openid-configuration.
|
||||
/// </summary>
|
||||
public string OidIssuer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the OIDC client id.
|
||||
/// </summary>
|
||||
public string OidClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the OIDC client secret.
|
||||
/// </summary>
|
||||
public string OidClientSecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the requested scopes, space separated.
|
||||
/// </summary>
|
||||
public string OidScopes { get; set; } = "openid profile email";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the claim used as the Jellyfin username.
|
||||
/// </summary>
|
||||
public string UsernameClaim { get; set; } = "preferred_username";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the claim containing the user's roles/groups.
|
||||
/// </summary>
|
||||
public string RoleClaim { get; set; } = "groups";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma separated list of role/group values that grant Jellyfin
|
||||
/// administrator rights. When empty, admin status is never modified by the plugin.
|
||||
/// </summary>
|
||||
public string AdminRoles { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma separated list of role/group values required to log in.
|
||||
/// When empty, every authenticated user may log in.
|
||||
/// </summary>
|
||||
public string AllowedRoles { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether unknown users are created on first login.
|
||||
/// </summary>
|
||||
public bool CreateUsersIfMissing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether auto-created users get a random password.
|
||||
/// Without one, Jellyfin accepts a blank password for them from any client.
|
||||
/// Users can still set their own password in the web profile for TV/native logins,
|
||||
/// or use Quick Connect.
|
||||
/// </summary>
|
||||
public bool SetRandomPasswordOnCreate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether strict discovery endpoint validation is disabled.
|
||||
/// Required for providers whose endpoints live on a different host than the issuer (e.g. Google).
|
||||
/// </summary>
|
||||
public bool DisableEndpointValidation { get; set; }
|
||||
}
|
||||
120
Jellyfin.Plugin.OidcAuth/Configuration/configPage.html
Normal file
120
Jellyfin.Plugin.OidcAuth/Configuration/configPage.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OIDC Auth</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="OidcAuthConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<form id="OidcAuthConfigForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">OIDC Auth</h2>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="OidIssuer" type="url" label="Issuer URL" />
|
||||
<div class="fieldDescription">Base URL of your OIDC provider, e.g. https://auth.example.com. The discovery document is loaded from <issuer>/.well-known/openid-configuration.</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="OidClientId" type="text" label="Client ID" />
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="OidClientSecret" type="password" label="Client Secret" autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="OidScopes" type="text" label="Scopes" />
|
||||
<div class="fieldDescription">Space separated. Default: openid profile email. Add e.g. "groups" if your provider needs it for the role claim.</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="UsernameClaim" type="text" label="Username claim" />
|
||||
<div class="fieldDescription">Claim used as Jellyfin username. Default: preferred_username. Falls back to "sub".</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="RoleClaim" type="text" label="Role claim" />
|
||||
<div class="fieldDescription">Claim containing groups/roles. Default: groups.</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="AllowedRoles" type="text" label="Allowed roles" />
|
||||
<div class="fieldDescription">Comma separated. Only users with one of these roles may log in. Empty = everyone.</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="AdminRoles" type="text" label="Admin roles" />
|
||||
<div class="fieldDescription">Comma separated. Users with one of these roles become Jellyfin administrators. Empty = never touch admin flag.</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input is="emby-checkbox" id="CreateUsersIfMissing" type="checkbox" />
|
||||
<span>Create users on first login</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input is="emby-checkbox" id="SetRandomPasswordOnCreate" type="checkbox" />
|
||||
<span>Set random password for auto-created users (recommended — without it, anyone can log in as them with a blank password)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input is="emby-checkbox" id="DisableEndpointValidation" type="checkbox" />
|
||||
<span>Disable strict endpoint validation (needed for Google and other providers whose endpoints are on a different host than the issuer)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="fieldDescription" style="margin-top: 1em;">
|
||||
Login URL: <code><your jellyfin address>/OidcAuth/login</code> —
|
||||
register <code><your jellyfin address>/OidcAuth/callback</code> as redirect URI at your provider.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var pluginId = '96badb44-9940-4ff0-befc-5f987159152c';
|
||||
var textFields = ['OidIssuer', 'OidClientId', 'OidClientSecret', 'OidScopes', 'UsernameClaim', 'RoleClaim', 'AllowedRoles', 'AdminRoles'];
|
||||
var boolFields = ['CreateUsersIfMissing', 'SetRandomPasswordOnCreate', 'DisableEndpointValidation'];
|
||||
|
||||
document.querySelector('#OidcAuthConfigPage').addEventListener('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
|
||||
textFields.forEach(function (f) { document.querySelector('#' + f).value = config[f] || ''; });
|
||||
boolFields.forEach(function (f) { document.querySelector('#' + f).checked = !!config[f]; });
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#OidcAuthConfigForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
|
||||
textFields.forEach(function (f) { config[f] = document.querySelector('#' + f).value; });
|
||||
boolFields.forEach(function (f) { config[f] = document.querySelector('#' + f).checked; });
|
||||
ApiClient.updatePluginConfiguration(pluginId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
});
|
||||
return false;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user