Compare commits

...
7 changed files with 311 additions and 75 deletions
+39 -7
View File
@@ -10,6 +10,15 @@ This is 100% alpha software! PRs are welcome to improve the code.
There is NO admin configuration! You must use the API to configure the program!
**This is for Jellyfin 10.8**
**This README reflects the __main__ branch! Switch tags to view version-specific documentation!**
## Tested Providers
- Google OpenID: Works, but usernames are all numeric
- Keycloak OpenID and SAML: Works
## Supported Protocols
- [OpenID](https://openid.net/what-is-openid/)
@@ -25,7 +34,7 @@ Add the package repo [https://repo.saggis.com/jellyfin/manifest.json](https://re
## Building
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll`, the `IdentityModel.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
## Releasing
@@ -33,13 +42,21 @@ This plugin uses [JPRM](https://github.com/oddstr13/jellyfin-plugin-repository-m
Build the zipped plugin with `jprm --verbosity=debug plugin build .`.
## Roadmap
- [ ] Admin page
- [ ] Automated tests
- [x] Add role/claims support
- [ ] Use canonical usernames instead of preferred usernames
- [ ] Finalize RBAC access for all user properties
## Examples
### SAML
Example for adding a SAML configuration with the API using [curl](https://curl.se/):
`curl -v -X POST -H "Content-Type: application/json" -d '{"samlEndpoint": "https://keycloak.example.com/auth/realms/test/protocol/saml", "samlClientId": "jellyfin-saml", "samlCertificate": "Very long base64 encoded string here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder1", "folder2"]}' "https://myjellyfin.example.com/sso/SAML/Add?api_key=API_KEY_HERE"`
`curl -v -X POST -H "Content-Type: application/json" -d '{"samlEndpoint": "https://keycloak.example.com/realms/test/protocol/saml", "samlClientId": "jellyfin-saml", "samlCertificate": "Very long base64 encoded string here", "enabled": true, "enableAuthorization": true, "enableAllFolders": false, "enabledFolders": [], "adminRoles": ["jellyfin-admin"], "roles": ["allowed-to-use-jellyfin"], "enableFolderRoles": true, "folderRoleMapping": [{"role": "allowed-to-watch-movies", "folders": ["cc7df17e2f3509a4b5fc1d1ff0a6c4d0", "f137a2dd21bbc1b99aa5c0f6bf02a805"]}]}' "https://myjellyfin.example.com/sso/SAML/Add?api_key=API_KEY_HERE"`
Make sure that the JSON is the same as the configuration you would like.
@@ -48,9 +65,9 @@ The SAML provider must have the following configuration (I am using Keycloak, an
- Sign Documents on
- Sign Assertions off
- Client Signature Required off
- Redirect URI: [https://myjellyfin.example.com/sso/OID/p/clientid](https://myjellyfin.example.com/sso/OID/p/clientid)
- Redirect URI: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/OID/p/clientid)
- Base URL: [https://myjellyfin.example.com](https://myjellyfin.example.com)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/saml/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid)
- Master SAML processing URL: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid)
Make sure that `clientid` is replaced with the actual client ID!
@@ -58,7 +75,7 @@ Make sure that `clientid` is replaced with the actual client ID!
Example for adding an OpenID configuration with the API using [curl](https://curl.se/)
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/auth/reapls/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder3", "folder4"]}' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAuthorization": true, "enableAllFolders": false, "enabledFolders": [], "adminRoles": ["jellyfin-admin"], "roles": ["allowed-to-use-jellyfin"], "enableFolderRoles": true, "folderRoleMapping": [{"role": "allowed-to-watch-movies", "folders": ["cc7df17e2f3509a4b5fc1d1ff0a6c4d0", "f137a2dd21bbc1b99aa5c0f6bf02a805"]}]' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
The OpenID provider must have the following configuration (again, I am using Keycloak)
@@ -89,15 +106,20 @@ The API is all done from a base URL of `/sso/`
#### Configuration
These all require authorization. Append an API key to the end of the request: `curl "http://myjellyfin.example.com/sso/SAML/Get?api_key=9c6e5fae4ae145669e6b7a3942f813b7"`
These all require authorization. Append an API key to the end of the request: `curl "http://myjellyfin.example.com/sso/SAML/Get?api_key=API_KEY_HERE"`
- POST `SAML/Add`: This adds a configuration for SAML. It accepts JSON with the following keys and format:
- `samlEndpoint`: string. The SAML endpoint.
- `samlClientId`: string. The SAML client ID.
- `samlCertificate`: string. The base64 encoded SAML certificate.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the SAML response against the `Role` attribute. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking.
- `adminRoles`: array of strings. This uses SAML response's `Role` attributes. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- GET `SAML/Del/clientId`: This removes a configuration for SAML for a given client ID.
- GET `SAML/Get`: Lists the configurations currently available.
@@ -125,12 +147,22 @@ These all require authorization. Append an API key to the end of the request: `c
- `oidClientId`: string. The OpenID client ID.
- `oidSecret`: string. The OpenID secret.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
- `adminRoles`: array of strings. This uses the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_roles` by default.
- GET `OID/Del/clientId`: This removes a configuration for OpenID for a given client ID.
- GET `OID/Get`: Lists the configurations currently available.
- GET `OID/States`: Lists currently active OpenID flows in progress.
### Misc
- POST `Unregister/username`: This "unregisters" a user from SSO. A JSON-formatted string must be posted with the new authentication provider. To reset to the default provider, use `Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider` like so: `curl -X POST -H "Content-Type: application/json" -d '"Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider"' "https://myjellyfin.example.com/sso/Unregister/username?api_key=API_KEY`
## Limitations
There is no GUI to sign in. You have to make it yourself! The buttons should redirect to something like this: [https://myjellyfin.example.com/sso/SAML/p/clientid](https://myjellyfin.example.com/sso/SAML/p/clientid) replacing `clientid` with the provider client ID and `SAML` with the auth scheme (either `SAML` or `OID`).
@@ -139,7 +171,7 @@ Furthermore, there is no functional admin page (yet). PRs for this are welcome.
There is also no logout callback. Logging out of Jellyfin will log you out of Jellyfin only, instead of the SSO provider as well.
This only supports Jellyfin on it's own domain (for now). This is because I'm using string concatenation for generating some URLs. A PR is welcome to patch this.
~~This only supports Jellyfin on it's own domain (for now). This is because I'm using string concatenation for generating some URLs. A PR is welcome to patch this.~~ Fixed in [PR #1](https://github.com/9p4/jellyfin-plugin-sso/pull/1).
**This only works on the web UI**. The user must open the Jellyfin web UI BEFORE using the SSO program to populate some values in the localStorage.
+193 -56
View File
@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Jellyfin.Plugin.SSO_Auth.Api;
@@ -41,39 +42,6 @@ public class SSOController : ControllerBase
_logger.LogInformation("SSO Controller initialized");
}
[HttpPost("SAML/p/{provider}")]
public ActionResult SAMLPost(string provider)
{
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (config.SamlClientId == provider && config.Enabled)
{
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
}
}
return Content("no active providers found"); // TODO: Return error code as well
}
[HttpGet("SAML/p/{provider}")]
public RedirectResult SAMLChallenge(string provider)
{
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (config.SamlClientId == provider && config.Enabled)
{
var request = new AuthRequest(
config.SamlClientId,
GetRequestBase() + "/sso/SAML/p/" + provider);
return Redirect(request.GetRedirectUrl(config.SamlEndpoint));
}
}
throw new ArgumentException("Provider does not exist");
}
[HttpGet("OID/r/{provider}")]
public ActionResult OIDPost(string provider)
{
@@ -88,8 +56,9 @@ public class SSOController : ControllerBase
ClientId = config.OIDClientId,
ClientSecret = config.OIDSecret,
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
Scope = "openid profile"
Scope = "openid profile",
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
var oidcClient = new OidcClient(options);
var state = StateManager[Request.Query["state"]].State;
var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, state).Result;
@@ -98,18 +67,90 @@ public class SSOController : ControllerBase
return Content("Something went wrong...", MediaTypeNames.Text.Plain);
}
if (!config.EnableFolderRoles)
{
StateManager[Request.Query["state"]].Folders = new List<string>(config.EnabledFolders);
} else {
StateManager[Request.Query["state"]].Folders = new List<string>();
}
foreach (var claim in result.User.Claims)
{
_logger.LogInformation("{0}: {1}", claim.Type, claim.Value);
if (claim.Type == "preferred_username")
{
StateManager[Request.Query["state"]].Valid = true;
StateManager[Request.Query["state"]].Username = claim.Value;
return Content(WebResponse.OIDGenerator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
if (config.Roles.Length == 0)
{
StateManager[Request.Query["state"]].Valid = true;
}
}
// Role processing
if (claim.Type == config.RoleClaim)
{
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
foreach (string role in roles)
{
// Check if allowed to login based on roles
if (config.Roles.Length != 0)
{
foreach (string validRoles in config.Roles)
{
if (role.Equals(validRoles))
{
StateManager[Request.Query["state"]].Valid = true;
}
}
}
// Check if admin based on roles
if (config.AdminRoles.Length != 0)
{
foreach (string validAdminRoles in config.AdminRoles)
{
if (role.Equals(validAdminRoles))
{
StateManager[Request.Query["state"]].Admin = true;
}
}
}
// Get allowed folders from roles
if (config.EnableFolderRoles)
{
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
if (role.Equals(folderRoleMap.Role))
{
StateManager[Request.Query["state"]].Folders.AddRange(folderRoleMap.Folders);
}
}
}
}
}
}
return Content("Does your OpenID provider not support the preferred_username value?", MediaTypeNames.Text.Plain);
// If the provider doesn't support preferred_username, then use sub
if (!StateManager[Request.Query["state"]].Valid)
{
foreach (var claim in result.User.Claims)
{
if (claim.Type == "sub")
{
StateManager[Request.Query["state"]].Username = claim.Value;
if (config.Roles.Length == 0)
{
StateManager[Request.Query["state"]].Valid = true;
}
}
}
}
if (StateManager[Request.Query["state"]].Valid)
{
return Content(WebResponse.OIDGenerator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
}
else
{
return Content("Error. Check permissions."); // TODO: Return error code as well
}
}
}
@@ -132,6 +173,7 @@ public class SSOController : ControllerBase
RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
Scope = "openid profile"
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
var oidcClient = new OidcClient(options);
var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
@@ -144,18 +186,18 @@ public class SSOController : ControllerBase
[Authorize(Policy = "RequiresElevation")]
[HttpPost("OID/Add")]
public void OIDAdd([FromBody] OIDConfig oidConfig)
public void OIDAdd([FromBody] OIDConfig config)
{
var configuration = SSOPlugin.Instance.Configuration;
for (var i = 0; i < configuration.OIDConfigs.Count; i++)
{
if (configuration.OIDConfigs[i].OIDClientId.Equals(oidConfig.OIDClientId))
if (configuration.OIDConfigs[i].OIDClientId.Equals(config.OIDClientId))
{
configuration.OIDConfigs.RemoveAt(i);
}
}
configuration.OIDConfigs.Add(oidConfig);
configuration.OIDConfigs.Add(config);
SSOPlugin.Instance.UpdateConfiguration(configuration);
}
@@ -194,15 +236,15 @@ public class SSOController : ControllerBase
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult> OIDAuth([FromBody] AuthResponse response)
{
foreach (var oidConfig in SSOPlugin.Instance.Configuration.OIDConfigs)
foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
{
if (oidConfig.OIDClientId == response.Provider && oidConfig.Enabled)
if (config.OIDClientId == response.Provider && config.Enabled)
{
foreach (var kvp in StateManager)
{
if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid)
{
var authenticationResult = await Authenticate(kvp.Value.Username, false, oidConfig.EnableAllFolders, oidConfig.EnabledFolders, response)
var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), response)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -213,20 +255,70 @@ public class SSOController : ControllerBase
return Problem("Something went wrong");
}
[HttpPost("SAML/p/{provider}")]
public ActionResult SAMLPost(string provider)
{
// I'm sure there's a better way than using nested for loops but eh whatever
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (config.SamlClientId == provider && config.Enabled)
{
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
// If no roles are configured, don't use RBAC
if (config.Roles.Length == 0)
{
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
}
// Check if user is allowed to log in based on roles
foreach (string role in samlResponse.GetCustomAttributes("Role"))
{
foreach (string allowedRole in config.Roles)
{
if (allowedRole.Equals(role))
{
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
}
}
}
return Content("401 Forbidden"); // TODO: Return error code as well
}
}
return Content("no active providers found"); // TODO: Return error code as well
}
[HttpGet("SAML/p/{provider}")]
public RedirectResult SAMLChallenge(string provider)
{
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (config.SamlClientId == provider && config.Enabled)
{
var request = new AuthRequest(
config.SamlClientId,
GetRequestBase() + "/sso/SAML/p/" + provider);
return Redirect(request.GetRedirectUrl(config.SamlEndpoint));
}
}
throw new ArgumentException("Provider does not exist");
}
[Authorize(Policy = "RequiresElevation")]
[HttpPost("SAML/Add")]
public void SamlAdd([FromBody] SamlConfig samlConfig)
public void SamlAdd([FromBody] SamlConfig config)
{
var configuration = SSOPlugin.Instance.Configuration;
for (var i = 0; i < configuration.SamlConfigs.Count; i++)
{
if (configuration.SamlConfigs[i].SamlClientId.Equals(samlConfig.SamlClientId))
if (configuration.SamlConfigs[i].SamlClientId.Equals(config.SamlClientId))
{
configuration.SamlConfigs.RemoveAt(i);
}
}
configuration.SamlConfigs.Add(samlConfig);
configuration.SamlConfigs.Add(config);
SSOPlugin.Instance.UpdateConfiguration(configuration);
}
@@ -258,12 +350,39 @@ public class SSOController : ControllerBase
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult> SamlAuth([FromBody] AuthResponse response)
{
foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (samlConfig.SamlClientId == response.Provider && samlConfig.Enabled)
if (config.SamlClientId == response.Provider && config.Enabled)
{
var samlResponse = new Response(samlConfig.SamlCertificate, response.Data);
var authenticationResult = await Authenticate(samlResponse.GetNameID(), false, samlConfig.EnableAllFolders, samlConfig.EnabledFolders, response)
bool isAdmin = false;
var samlResponse = new Response(config.SamlCertificate, response.Data);
List<string> folders;
if (!config.EnableFolderRoles)
{
folders = new List<string>(config.EnabledFolders);
} else {
folders = new List<string>();
}
foreach (string role in samlResponse.GetCustomAttributes("Role"))
{
foreach (string allowedRole in config.AdminRoles)
{
if (allowedRole.Equals(role))
{
isAdmin = true;
}
}
if (config.EnableFolderRoles) {
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
if (folderRoleMap.Role.Equals(role)) {
folders.AddRange(folderRoleMap.Folders);
}
}
}
}
var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), response)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -272,9 +391,18 @@ public class SSOController : ControllerBase
return Problem("Something went wrong");
}
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
[Authorize(Policy = "RequiresElevation")]
[HttpPost("Unregister/{username}")]
public ActionResult Unregister(string username, [FromBody] string provider)
{
User user = _userManager.GetUserByName(username);
user.AuthenticationProviderId = provider;
return Ok();
}
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
{
_logger.LogInformation("Authenticating");
User user = null;
user = _userManager.GetUserByName(username);
@@ -282,17 +410,19 @@ public class SSOController : ControllerBase
{
_logger.LogInformation("SSO user doesn't exist, creating...");
user = await _userManager.CreateUserAsync(username).ConfigureAwait(false);
user.AuthenticationProviderId = GetType().FullName;
}
user.AuthenticationProviderId = GetType().FullName;
if (enableAuthorization) {
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
if (!enableAllFolders)
{
user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
}
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
var authRequest = new AuthenticationRequest();
authRequest.UserId = user.Id;
authRequest.Username = user.Username;
@@ -344,6 +474,7 @@ public class TimedAuthorizeState
State = state;
Created = created;
Valid = false;
Admin = false;
}
public AuthorizeState State { get; set; }
@@ -353,4 +484,10 @@ public class TimedAuthorizeState
public bool Valid { get; set; }
public string Username { get; set; }
public bool Admin { get; set; }
public string Email { get; set; }
public List<string> Folders { get; set; }
}
+33
View File
@@ -37,9 +37,21 @@ public class SamlConfig
public bool Enabled { get; set; }
public bool EnableAuthorization { get; set; }
public bool EnableAllFolders { get; set; }
public string[] EnabledFolders { get; set; }
public string[] AdminRoles { get; set; }
public string[] Roles { get; set; }
public bool EnableFolderRoles { get; set; }
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List<FolderRoleMap> FolderRoleMapping { get; set; }
}
[XmlRoot("PluginConfiguration")]
@@ -53,7 +65,28 @@ public class OIDConfig
public bool Enabled { get; set; }
public bool EnableAuthorization { get; set; }
public bool EnableAllFolders { get; set; }
public string[] EnabledFolders { get; set; }
public string[] AdminRoles { get; set; }
public string[] Roles { get; set; }
public bool EnableFolderRoles { get; set; }
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List<FolderRoleMap> FolderRoleMapping { get; set; }
public string RoleClaim { get; set; }
}
public class FolderRoleMap
{
public string Role { get; set; }
public List<string> Folders { get; set; }
}
+21 -4
View File
@@ -11,10 +11,15 @@
<div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">SSO Settings:</h2>
<a is="emby-button" class="raised button-alt headerHelpButton" target="_blank" href="https://github.com/sambhavsaggi/jellyfin-plugin-sso">${Help}</a>
<a is="emby-button" class="raised button-alt headerHelpButton" target="_blank" href="https://github.com/9p4/jellyfin-plugin-sso">${Help}</a>
</div>
<p><i>Note:</i> Making changes to this configuration requires a restart of Jellyfin.</p>
<div class="verticalSection" is="emby-collapse" title="SSO Server Settings">
<button id="newSaml" is="emby-button">
<span>Add new SAML provider</span>
</button>
<button id="newOID" is="emby-button">
<span>Add new OpenID provider</span>
</button>
<div class="collapseContent" id="default">
<div class="samlProviderWrapper">
<div class="samlProvider">
@@ -37,7 +42,7 @@
</div>
<button id="btnSaveSettings" is="emby-button" type="submit" value="submit" class="raised button block">
<span>Save SSO Settings</span>
<span>Update Provider</span>
</button>
</div>
</div>
@@ -50,7 +55,6 @@
<script type="text/javascript">
var SSOConfigurationPage = {
pluginUniqueId: "505ce9d1-d916-42fa-86ca-673ef241d7df",
samlProviderWrapper: document.querySelector("#samlProviderWrapper")
};
document.querySelector('.esqConfigurationPage').addEventListener("pageshow", function () {
@@ -80,6 +84,19 @@
// Disable default form submission
return false;
});
var newSaml = document.getElementById("newSaml");
newSaml.addEventListener("click", function(e) {
e.preventDefault();
Dashboard.showLoadingMsg();
fetch(window.ApiClient.getUrl("sso/SAML/Get?api_key=" + window.ApiClient.accessToken()))
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
}).then(json => {console.log(json)});
});
</script>
</div>
</body>
+3 -2
View File
@@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.SSO_Auth</RootNamespace>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>2.0.1.0</AssemblyVersion>
<FileVersion>2.0.1.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
@@ -19,6 +19,7 @@
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.0" />
</ItemGroup>
+13 -1
View File
@@ -1,5 +1,5 @@
/*
Jitbit's simple SAML 2.0 component for ASP.NET
Was Jitbit's simple SAML 2.0 component for ASP.NET
https://github.com/jitbit/AspNetSaml/
(c) Jitbit LP, 2016
Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
@@ -7,6 +7,7 @@
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography.X509Certificates;
@@ -188,6 +189,17 @@ public class Response
return node?.InnerText;
}
public List<string> GetCustomAttributes(string attr)
{
var node = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
List<string> output = new List<string>();
foreach (XmlNode item in node)
{
output.Add(item?.InnerText);
}
return output;
}
// returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces
// see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary
private XmlNamespaceManager GetNamespaceManager()
+9 -5
View File
@@ -1,16 +1,20 @@
name: "SSO Authentication"
guid: "505ce9d1-d916-42fa-86ca-673ef241d7df"
imageUrl: "https://raw.githubusercontent.com/sambhavsaggi/jellyfin-plugin-sso/main/img/logo.png"
version: "1"
imageUrl: "https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/main/img/logo.png"
version: "2.0.1.0"
targetAbi: "10.8.0.0"
framework: "net6.0"
owner: "sambhavsaggi"
owner: "9p4"
overview: "Authenticate users against an SSO provider."
description: |
This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
Review documentation at https://github.com/sambhavsaggi/jellyfin-plugin-sso
Review documentation at https://github.com/9p4/jellyfin-plugin-sso
category: "Authentication"
artifacts:
- "SSO-Auth.dll"
- "IdentityModel.OidcClient.dll"
changelog: "2022-01-16: Initial Release"
- "IdentityModel.dll"
changelog: |
2.0.1.0: Fix improper artifact loading
2.0.0.0: Add RBAC and Google support
1.0.0.0: Initial Release