diff --git a/SSO-Auth/Api/SSOController.cs b/SSO-Auth/Api/SSOController.cs
index a5edebf..2a7f3b1 100644
--- a/SSO-Auth/Api/SSOController.cs
+++ b/SSO-Auth/Api/SSOController.cs
@@ -1,327 +1,356 @@
using System;
-using System.Net.Mime;
using System.Collections.Generic;
-using MediaBrowser.Controller.Session;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Microsoft.AspNetCore.Authorization;
-using IdentityModel.OidcClient;
-using Jellyfin.Plugin.SSO_Auth.Config;
-using Saml;
-using MediaBrowser.Common;
+using System.Net.Mime;
using System.Threading.Tasks;
+using IdentityModel.OidcClient;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Plugin.SSO_Auth.Config;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
-namespace Jellyfin.Plugin.SSO_Auth.Api
+namespace Jellyfin.Plugin.SSO_Auth.Api;
+
+///
+/// The sso api controller.
+///
+[ApiController]
+[Route("[controller]")]
+public class SSOController : ControllerBase
{
+ private readonly IUserManager _userManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly ILogger _logger;
+ private static readonly IDictionary StateManager = new Dictionary();
+
///
- /// The sso api controller.
+ /// Initializes a new instance of the class.
///
- [ApiController]
- [Route("[controller]")]
- public class SSOController : ControllerBase
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public SSOController(ILogger logger, ISessionManager sessionManager, IUserManager userManager)
{
- private readonly IApplicationHost _applicationHost;
- private readonly ISessionManager _sessionManager;
- private readonly ILogger _logger;
- private static IDictionary _stateManager = new Dictionary();
+ _sessionManager = sessionManager;
+ _userManager = userManager;
+ _logger = logger;
+ _logger.LogInformation("SSO Controller initialized");
+ }
- public SSOController(IApplicationHost appHost, ILoggerFactory loggerFactory, ISessionManager sessionManager)
+ [HttpPost("SAML/p/{provider}")]
+ public ActionResult SAMLPost(string provider)
+ {
+ foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
- _applicationHost = appHost;
- _sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger();
- _logger.LogWarning("SSO Controller initialized");
- }
-
- [HttpPost("SAML/p/{provider}")]
- public ActionResult SAMLPost(string provider)
- {
- foreach (SamlConfig config in SSOPlugin.Instance.Configuration.SamlConfigs)
+ if (config.SamlClientId == provider && config.Enabled)
{
- if (config.SamlClientId == provider && config.Enabled)
- {
- Saml.Response samlResponse = new Saml.Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
- return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.UTF8Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider), "text/html");
- }
+ 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), MediaTypeNames.Text.Html);
}
- return Content("no active providers found"); // TODO: Return error code as well
}
- [HttpGet("SAML/p/{provider}")]
- public RedirectResult SAMLChallenge(string provider)
+ 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)
{
- foreach (SamlConfig config in SSOPlugin.Instance.Configuration.SamlConfigs)
+ if (config.SamlClientId == provider && config.Enabled)
{
- if (config.SamlClientId == provider && config.Enabled)
- {
- var request = new AuthRequest(
- config.SamlClientId,
- "http://" + Request.Host.Value + "/sso/SAML/p/" + provider
- );
- return Redirect(request.GetRedirectUrl(config.SamlEndpoint));
- }
+ 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)
+ throw new ArgumentException("Provider does not exist");
+ }
+
+ [HttpGet("OID/r/{provider}")]
+ public ActionResult OIDPost(string provider)
+ {
+ // Actually a GET: https://github.com/IdentityModel/IdentityModel.OidcClient/issues/325
+ foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
{
- // Actually a GET: https://github.com/IdentityModel/IdentityModel.OidcClient/issues/325
- foreach (OIDConfig config in SSOPlugin.Instance.Configuration.OIDConfigs)
+ if (config.OIDClientId == provider && config.Enabled)
{
- if (config.OIDClientId == provider && config.Enabled)
+ var options = new OidcClientOptions
{
- var options = new OidcClientOptions
+ Authority = config.OIDEndpoint,
+ ClientId = config.OIDClientId,
+ ClientSecret = config.OIDSecret,
+ RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
+ Scope = "openid profile"
+ };
+ var oidcClient = new OidcClient(options);
+ var state = StateManager[Request.Query["state"]].State;
+ var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, state).Result;
+ if (result.IsError)
+ {
+ return Content("Something went wrong...", MediaTypeNames.Text.Plain);
+ }
+
+ foreach (var claim in result.User.Claims)
+ {
+ _logger.LogInformation("{0}: {1}", claim.Type, claim.Value);
+ if (claim.Type == "preferred_username")
{
- Authority = config.OIDEndpoint,
- ClientId = config.OIDClientId,
- ClientSecret = config.OIDSecret,
- RedirectUri = "http://" + Request.Host.Value + "/sso/OID/r/" + provider,
- Scope = "openid profile"
- };
- OidcClient oidcClient = new OidcClient(options);
- var state = _stateManager[Request.Query["state"]].State;
- var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, state).Result;
- if (result.IsError)
- {
- return Content("Something went wrong...", "text/plain");
+ StateManager[Request.Query["state"]].Valid = true;
+ StateManager[Request.Query["state"]].Username = claim.Value;
+ return Content(WebResponse.OIDGenerator(data: Request.Query["state"], provider: provider), MediaTypeNames.Text.Html);
}
- foreach (var claim in result.User.Claims)
+ }
+
+ return Content("Does your OpenID provider not support the preferred_username value?", MediaTypeNames.Text.Plain);
+ }
+ }
+
+ return Content("no active providers found"); // TODO: Return error code as well
+ }
+
+ [HttpGet("OID/p/{provider}")]
+ public async Task OIDChallenge(string provider)
+ {
+ Invalidate();
+ foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
+ {
+ if (config.OIDClientId == provider && config.Enabled)
+ {
+ var options = new OidcClientOptions
+ {
+ Authority = config.OIDEndpoint,
+ ClientId = config.OIDClientId,
+ ClientSecret = config.OIDSecret,
+ RedirectUri = GetRequestBase() + "/sso/OID/r/" + provider,
+ Scope = "openid profile"
+ };
+ var oidcClient = new OidcClient(options);
+ var state = await oidcClient.PrepareLoginAsync().ConfigureAwait(false);
+ StateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
+ return Redirect(state.StartUrl);
+ }
+ }
+
+ throw new ArgumentException("Provider does not exist");
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpPost("OID/Add")]
+ public void OIDAdd([FromBody] OIDConfig oidConfig)
+ {
+ var configuration = SSOPlugin.Instance.Configuration;
+ for (var i = 0; i < configuration.OIDConfigs.Count; i++)
+ {
+ if (configuration.OIDConfigs[i].OIDClientId.Equals(oidConfig.OIDClientId))
+ {
+ configuration.OIDConfigs.RemoveAt(i);
+ }
+ }
+
+ configuration.OIDConfigs.Add(oidConfig);
+ SSOPlugin.Instance.UpdateConfiguration(configuration);
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpGet("OID/Del/{provider}")]
+ public void OIDDel(string provider)
+ {
+ var configuration = SSOPlugin.Instance.Configuration;
+ for (var i = 0; i < configuration.OIDConfigs.Count; i++)
+ {
+ if (configuration.OIDConfigs[i].OIDClientId.Equals(provider))
+ {
+ configuration.OIDConfigs.RemoveAt(i);
+ }
+ }
+
+ SSOPlugin.Instance.UpdateConfiguration(configuration);
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpGet("OID/Get")]
+ public ActionResult OIDProviders()
+ {
+ return Ok(SSOPlugin.Instance.Configuration.OIDConfigs);
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpGet("OID/States")]
+ public ActionResult OIDStates()
+ {
+ return Ok(StateManager);
+ }
+
+ [HttpPost("OID/Auth")]
+ [Consumes(MediaTypeNames.Application.Json)]
+ [Produces(MediaTypeNames.Application.Json)]
+ public async Task OIDAuth([FromBody] AuthResponse response)
+ {
+ foreach (var oidConfig in SSOPlugin.Instance.Configuration.OIDConfigs)
+ {
+ if (oidConfig.OIDClientId == response.Provider && oidConfig.Enabled)
+ {
+ foreach (var kvp in StateManager)
+ {
+ if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid)
{
- _logger.LogWarning("{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), "text/html");
- }
- }
- return Content("Does your OpenID provider not support the preferred_username value?", "text/plain");
- }
- }
- return Content("no active providers found"); // TODO: Return error code as well
- }
-
- [HttpGet("OID/p/{provider}")]
- public ActionResult OIDChallenge(string provider)
- {
- Invalidate();
- foreach (OIDConfig config in SSOPlugin.Instance.Configuration.OIDConfigs)
- {
- if (config.OIDClientId == provider && config.Enabled)
- {
- var options = new OidcClientOptions
- {
- Authority = config.OIDEndpoint,
- ClientId = config.OIDClientId,
- ClientSecret = config.OIDSecret,
- RedirectUri = "http://" + Request.Host.Value + "/sso/OID/r/" + provider,
- Scope = "openid profile"
- };
- OidcClient oidcClient = new OidcClient(options);
- AuthorizeState state = oidcClient.PrepareLoginAsync().Result;
- _stateManager.Add(state.State, new TimedAuthorizeState(state, DateTime.Now));
- return Redirect(state.StartUrl);
- }
- }
- throw new ArgumentException("Provider does not exist");
- }
-
- [Authorize(Policy = "RequiresElevation")]
- [HttpPost("OID/Add")]
- public void OIDAdd([FromBody] OIDConfig oidConfig)
- {
- var configuration = SSOPlugin.Instance.Configuration;
- for (int i = 0; i < configuration.OIDConfigs.Count; i++)
- {
- if (configuration.OIDConfigs[i].OIDClientId.Equals(oidConfig.OIDClientId))
- {
- configuration.OIDConfigs.RemoveAt(i);
- }
- }
- configuration.OIDConfigs.Add(oidConfig);
- SSOPlugin.Instance.UpdateConfiguration(configuration);
- }
-
- [Authorize(Policy = "RequiresElevation")]
- [HttpGet("OID/Del/{provider}")]
- public void OIDDel(string provider)
- {
- var configuration = SSOPlugin.Instance.Configuration;
- for (int i = 0; i < configuration.OIDConfigs.Count; i++)
- {
- if (configuration.OIDConfigs[i].OIDClientId.Equals(provider))
- {
- configuration.OIDConfigs.RemoveAt(i);
- }
- }
- SSOPlugin.Instance.UpdateConfiguration(configuration);
- }
-
-
- [Authorize(Policy = "RequiresElevation")]
- [HttpGet("OID/Get")]
- public ActionResult OIDProviders()
- {
- return Ok(SSOPlugin.Instance.Configuration.OIDConfigs);
- }
-
- [Authorize(Policy = "RequiresElevation")]
- [HttpGet("OID/States")]
- public ActionResult OIDStates()
- {
- return Ok(_stateManager);
- }
-
- [HttpPost("OID/Auth")]
- [Consumes(MediaTypeNames.Application.Json)]
- [Produces(MediaTypeNames.Application.Json)]
- public ActionResult OIDAuth([FromBody] AuthResponse response)
- {
- foreach (OIDConfig oidConfig in SSOPlugin.Instance.Configuration.OIDConfigs)
- {
- if (oidConfig.OIDClientId == response.Provider && oidConfig.Enabled)
- {
- foreach (KeyValuePair kvp in _stateManager)
- {
- if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid) {
- AuthenticationResult authenticationResult = Authenticate(kvp.Value.Username, false, oidConfig.EnableAllFolders, oidConfig.EnabledFolders, response).Result;
+ var authenticationResult = await Authenticate(kvp.Value.Username, false, oidConfig.EnableAllFolders, oidConfig.EnabledFolders, response)
+ .ConfigureAwait(false);
return Ok(authenticationResult);
- }
}
}
}
- return Problem("Something went wrong");
}
- [Authorize(Policy = "RequiresElevation")]
- [HttpPost("SAML/Add")]
- public void SamlAdd([FromBody] SamlConfig samlConfig)
+ return Problem("Something went wrong");
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpPost("SAML/Add")]
+ public void SamlAdd([FromBody] SamlConfig samlConfig)
+ {
+ var configuration = SSOPlugin.Instance.Configuration;
+ for (var i = 0; i < configuration.SamlConfigs.Count; i++)
{
- var configuration = SSOPlugin.Instance.Configuration;
- for (int i = 0; i < configuration.SamlConfigs.Count; i++)
+ if (configuration.SamlConfigs[i].SamlClientId.Equals(samlConfig.SamlClientId))
{
- if (configuration.SamlConfigs[i].SamlClientId.Equals(samlConfig.SamlClientId))
- {
- configuration.SamlConfigs.RemoveAt(i);
- }
+ configuration.SamlConfigs.RemoveAt(i);
}
- configuration.SamlConfigs.Add(samlConfig);
- SSOPlugin.Instance.UpdateConfiguration(configuration);
}
- [Authorize(Policy = "RequiresElevation")]
- [HttpGet("SAML/Del/{provider}")]
- public void SamlDel(string provider)
+ configuration.SamlConfigs.Add(samlConfig);
+ SSOPlugin.Instance.UpdateConfiguration(configuration);
+ }
+
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpGet("SAML/Del/{provider}")]
+ public void SamlDel(string provider)
+ {
+ var configuration = SSOPlugin.Instance.Configuration;
+ for (var i = 0; i < configuration.SamlConfigs.Count; i++)
{
- var configuration = SSOPlugin.Instance.Configuration;
- for (int i = 0; i < configuration.SamlConfigs.Count; i++)
+ if (configuration.SamlConfigs[i].SamlClientId.Equals(provider))
{
- if (configuration.SamlConfigs[i].SamlClientId.Equals(provider))
- {
- configuration.SamlConfigs.RemoveAt(i);
- }
+ configuration.SamlConfigs.RemoveAt(i);
}
- SSOPlugin.Instance.UpdateConfiguration(configuration);
}
+ SSOPlugin.Instance.UpdateConfiguration(configuration);
+ }
- [Authorize(Policy = "RequiresElevation")]
- [HttpGet("SAML/Get")]
- public ActionResult SamlProviders()
- {
- return Ok(SSOPlugin.Instance.Configuration.SamlConfigs);
- }
+ [Authorize(Policy = "RequiresElevation")]
+ [HttpGet("SAML/Get")]
+ public ActionResult SamlProviders()
+ {
+ return Ok(SSOPlugin.Instance.Configuration.SamlConfigs);
+ }
- [HttpPost("SAML/Auth")]
- [Consumes(MediaTypeNames.Application.Json)]
- [Produces(MediaTypeNames.Application.Json)]
- public ActionResult SamlAuth([FromBody] AuthResponse response)
+ [HttpPost("SAML/Auth")]
+ [Consumes(MediaTypeNames.Application.Json)]
+ [Produces(MediaTypeNames.Application.Json)]
+ public async Task SamlAuth([FromBody] AuthResponse response)
+ {
+ foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
{
- foreach (SamlConfig samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
+ if (samlConfig.SamlClientId == response.Provider && samlConfig.Enabled)
{
- if (samlConfig.SamlClientId == response.Provider && samlConfig.Enabled)
- {
- Saml.Response samlResponse = new Saml.Response(samlConfig.SamlCertificate, response.Data);
- AuthenticationResult authenticationResult = Authenticate(samlResponse.GetNameID(), false, samlConfig.EnableAllFolders, samlConfig.EnabledFolders, response).Result;
- return Ok(authenticationResult);
- }
+ var samlResponse = new Response(samlConfig.SamlCertificate, response.Data);
+ var authenticationResult = await Authenticate(samlResponse.GetNameID(), false, samlConfig.EnableAllFolders, samlConfig.EnabledFolders, response)
+ .ConfigureAwait(false);
+ return Ok(authenticationResult);
}
- return Problem("Something went wrong");
}
- private async Task Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
+ return Problem("Something went wrong");
+ }
+
+ private async Task Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
+ {
+ _logger.LogInformation("Authenticating");
+ User user = null;
+ user = _userManager.GetUserByName(username);
+
+ if (user == null)
{
- _logger.LogWarning("Authenticating");
- var userManager = _applicationHost.Resolve();
- User user = null;
- user = userManager.GetUserByName(username);
-
- if (user == null)
+ _logger.LogInformation("SSO user doesn't exist, creating...");
+ user = await _userManager.CreateUserAsync(username).ConfigureAwait(false);
+ user.AuthenticationProviderId = GetType().FullName;
+ user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+ user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
+ if (!enableAllFolders)
{
- _logger.LogWarning("SSO user doesn't exist, creating...");
- user = await userManager.CreateUserAsync(username).ConfigureAwait(false);
- user.AuthenticationProviderId = GetType().FullName;
- user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
- user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
- if (!enableAllFolders)
- {
- user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
- }
-
- await userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
}
- AuthenticationRequest authRequest = new AuthenticationRequest();
- authRequest.UserId = user.Id;
- authRequest.Username = user.Username;
- authRequest.App = authResponse.AppName;
- authRequest.AppVersion = authResponse.AppVersion;
- authRequest.DeviceId = authResponse.DeviceID;
- authRequest.DeviceName = authResponse.DeviceName;
- _logger.LogWarning("Auth request created...");
- return _sessionManager.AuthenticateDirect(authRequest).Result;
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
- private void Invalidate()
+ var authRequest = new AuthenticationRequest();
+ authRequest.UserId = user.Id;
+ authRequest.Username = user.Username;
+ authRequest.App = authResponse.AppName;
+ authRequest.AppVersion = authResponse.AppVersion;
+ authRequest.DeviceId = authResponse.DeviceID;
+ authRequest.DeviceName = authResponse.DeviceName;
+ _logger.LogInformation("Auth request created...");
+ return await _sessionManager.AuthenticateDirect(authRequest).ConfigureAwait(false);
+ }
+
+ private void Invalidate()
+ {
+ foreach (var kvp in StateManager)
{
- foreach (KeyValuePair kvp in _stateManager)
+ var now = DateTime.Now;
+ if (now.Subtract(kvp.Value.Created).TotalMinutes > 1)
{
- DateTime now = DateTime.Now;
- if (now.Subtract(kvp.Value.Created).TotalMinutes > 1)
- {
- _stateManager.Remove(kvp.Key);
- }
+ StateManager.Remove(kvp.Key);
}
}
}
- public class AuthResponse
+ private string GetRequestBase()
{
- public string DeviceID { get; set; }
- public string DeviceName { get; set; }
- public string AppName { get; set; }
- public string AppVersion { get; set; }
- public string Data { get; set; }
- public string Provider { get; set; }
- }
-
- public class TimedAuthorizeState
- {
- public TimedAuthorizeState(AuthorizeState state, DateTime created)
- {
- this.State = state;
- this.Created = created;
- this.Valid = false;
- }
- public AuthorizeState State { get; set; }
- public DateTime Created { get; set; }
- public bool Valid { get; set; }
- public string Username { get; set; }
+ return Request.Scheme + "://" + Request.Host + Request.PathBase;
}
}
+
+public class AuthResponse
+{
+ public string DeviceID { get; set; }
+
+ public string DeviceName { get; set; }
+
+ public string AppName { get; set; }
+
+ public string AppVersion { get; set; }
+
+ public string Data { get; set; }
+
+ public string Provider { get; set; }
+}
+
+public class TimedAuthorizeState
+{
+ public TimedAuthorizeState(AuthorizeState state, DateTime created)
+ {
+ State = state;
+ Created = created;
+ Valid = false;
+ }
+
+ public AuthorizeState State { get; set; }
+
+ public DateTime Created { get; set; }
+
+ public bool Valid { get; set; }
+
+ public string Username { get; set; }
+}
diff --git a/SSO-Auth/Config/PluginConfiguration.cs b/SSO-Auth/Config/PluginConfiguration.cs
index 1f90502..c2c0d09 100644
--- a/SSO-Auth/Config/PluginConfiguration.cs
+++ b/SSO-Auth/Config/PluginConfiguration.cs
@@ -1,57 +1,59 @@
using System.Collections.Generic;
using System.Xml.Serialization;
-namespace Jellyfin.Plugin.SSO_Auth.Config {
+namespace Jellyfin.Plugin.SSO_Auth.Config;
+
+///
+/// Plugin Configuration.
+///
+public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration
+{
///
- /// Plugin Configuration.
+ /// Initializes a new instance of the class.
///
- public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration {
- ///
- /// Initializes a new instance of the class.
- ///
- public PluginConfiguration()
- {
- SamlConfigs = new List();
- OIDConfigs = new List();
- }
-
- [XmlArray("SamlConfigs"), XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
- public List SamlConfigs { get; set; }
-
- [XmlArray("OIDConfigs"), XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
- public List OIDConfigs { get; set; }
-
- }
-
- [XmlRoot("PluginConfiguration")]
- public class SamlConfig
+ public PluginConfiguration()
{
- public string SamlEndpoint { get; set; }
-
- public string SamlClientId { get; set; }
-
- public string SamlCertificate { get; set; }
-
- public bool Enabled { get; set; }
-
- public bool EnableAllFolders { get; set; }
-
- public string[] EnabledFolders { get; set; }
+ SamlConfigs = new List();
+ OIDConfigs = new List();
}
- [XmlRoot("PluginConfiguration")]
- public class OIDConfig
- {
- public string OIDEndpoint { get; set; }
+ [XmlArray("SamlConfigs")]
+ [XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
+ public List SamlConfigs { get; set; }
- public string OIDClientId { get; set; }
-
- public string OIDSecret { get; set; }
-
- public bool Enabled { get; set; }
-
- public bool EnableAllFolders { get; set; }
-
- public string[] EnabledFolders { get; set; }
- }
+ [XmlArray("OIDConfigs")]
+ [XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
+ public List OIDConfigs { get; set; }
+}
+
+[XmlRoot("PluginConfiguration")]
+public class SamlConfig
+{
+ public string SamlEndpoint { get; set; }
+
+ public string SamlClientId { get; set; }
+
+ public string SamlCertificate { get; set; }
+
+ public bool Enabled { get; set; }
+
+ public bool EnableAllFolders { get; set; }
+
+ public string[] EnabledFolders { get; set; }
+}
+
+[XmlRoot("PluginConfiguration")]
+public class OIDConfig
+{
+ public string OIDEndpoint { get; set; }
+
+ public string OIDClientId { get; set; }
+
+ public string OIDSecret { get; set; }
+
+ public bool Enabled { get; set; }
+
+ public bool EnableAllFolders { get; set; }
+
+ public string[] EnabledFolders { get; set; }
}
diff --git a/SSO-Auth/SSO-Auth.csproj b/SSO-Auth/SSO-Auth.csproj
index c980519..a3b33d1 100644
--- a/SSO-Auth/SSO-Auth.csproj
+++ b/SSO-Auth/SSO-Auth.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/SSO-Auth/SSOPlugin.cs b/SSO-Auth/SSOPlugin.cs
index f63ea53..967f574 100644
--- a/SSO-Auth/SSOPlugin.cs
+++ b/SSO-Auth/SSOPlugin.cs
@@ -6,10 +6,14 @@ using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
-namespace Jellyfin.Plugin.SSO_Auth {
- public class SSOPlugin : BasePlugin, IHasWebPages {
- public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) {
- Instance = this;
+namespace Jellyfin.Plugin.SSO_Auth;
+
+public class SSOPlugin : BasePlugin, IHasWebPages
+{
+ public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
}
public static SSOPlugin Instance { get; private set; }
@@ -18,13 +22,12 @@ namespace Jellyfin.Plugin.SSO_Auth {
public override Guid Id => Guid.Parse("505ce9d1-d916-42fa-86ca-673ef241d7df");
- public IEnumerable GetPages() {
- return new[] {
- new PluginPageInfo {
- Name = Name,
- EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
- }
- };
+ public IEnumerable GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
+ };
}
- }
}
diff --git a/SSO-Auth/Saml.cs b/SSO-Auth/Saml.cs
index b02ecc6..8e4321d 100644
--- a/SSO-Auth/Saml.cs
+++ b/SSO-Auth/Saml.cs
@@ -1,273 +1,301 @@
-/* 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/)
- version 1.2.3
+/*
+ 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/)
+ version 1.2.3
*/
using System;
-using System.Web;
using System.IO;
-using System.Xml;
+using System.IO.Compression;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
-using System.IO.Compression;
using System.Text;
+using System.Web;
+using System.Xml;
-namespace Saml
+namespace Jellyfin.Plugin.SSO_Auth;
+
+public class Response
{
- public partial class Response
- {
- protected XmlDocument _xmlDoc;
- protected readonly X509Certificate2 _certificate;
- protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML
+ private readonly X509Certificate2 _certificate;
+ private XmlDocument _xmlDoc;
+ private XmlNamespaceManager _xmlNameSpaceManager; // we need this one to run our XPath queries on the SAML XML
- public string Xml { get { return _xmlDoc.OuterXml; } }
+ public Response(string certificateStr, string responseString)
+ : this(Convert.FromBase64String(certificateStr), responseString)
+ {
+ }
- public Response(string certificateStr, string responseString)
- : this(Convert.FromBase64String(certificateStr), responseString) { }
+ public Response(byte[] certificateBytes, string responseString) : this(certificateBytes)
+ {
+ LoadXmlFromBase64(responseString);
+ }
- public Response(byte[] certificateBytes, string responseString) : this(certificateBytes)
- {
- LoadXmlFromBase64(responseString);
- }
+ public Response(string certificateStr) : this(Convert.FromBase64String(certificateStr))
+ {
+ }
- public Response(string certificateStr) : this(Convert.FromBase64String(certificateStr)) { }
+ public Response(byte[] certificateBytes)
+ {
+ _certificate = new X509Certificate2(certificateBytes);
+ }
- public Response(byte[] certificateBytes)
- {
- _certificate = new X509Certificate2(certificateBytes);
- }
+ public string Xml => _xmlDoc.OuterXml;
- public void LoadXml(string xml)
- {
- _xmlDoc = new XmlDocument();
- _xmlDoc.PreserveWhitespace = true;
- _xmlDoc.XmlResolver = null;
- _xmlDoc.LoadXml(xml);
+ public void LoadXml(string xml)
+ {
+ _xmlDoc = new XmlDocument();
+ _xmlDoc.PreserveWhitespace = true;
+ _xmlDoc.XmlResolver = null;
+ _xmlDoc.LoadXml(xml);
- _xmlNameSpaceManager = GetNamespaceManager(); //lets construct a "manager" for XPath queries
- }
+ _xmlNameSpaceManager = GetNamespaceManager(); // lets construct a "manager" for XPath queries
+ }
- public void LoadXmlFromBase64(string response)
- {
- LoadXml(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(response)));
- }
+ public void LoadXmlFromBase64(string response)
+ {
+ LoadXml(Encoding.UTF8.GetString(Convert.FromBase64String(response)));
+ }
- public bool IsValid()
- {
- XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
+ public bool IsValid()
+ {
+ var nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
- SignedXml signedXml = new SignedXml(_xmlDoc);
+ var signedXml = new SignedXml(_xmlDoc);
- if (nodeList.Count == 0) return false;
+ if (nodeList.Count == 0)
+ {
+ return false;
+ }
- signedXml.LoadXml((XmlElement)nodeList[0]);
- return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
- }
+ signedXml.LoadXml((XmlElement)nodeList[0]);
+ return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
+ }
- //an XML signature can "cover" not the whole document, but only a part of it
- //.NET's built in "CheckSignature" does not cover this case, it will validate to true.
- //We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack
- private bool ValidateSignatureReference(SignedXml signedXml)
- {
- if (signedXml.SignedInfo.References.Count != 1) //no ref at all
- return false;
+ // an XML signature can "cover" not the whole document, but only a part of it
+ // .NET's built in "CheckSignature" does not cover this case, it will validate to true.
+ // We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack
+ private bool ValidateSignatureReference(SignedXml signedXml)
+ {
+ if (signedXml.SignedInfo.References.Count != 1) // no ref at all
+ {
+ return false;
+ }
- var reference = (Reference)signedXml.SignedInfo.References[0];
- var id = reference.Uri.Substring(1);
+ var reference = (Reference)signedXml.SignedInfo.References[0];
+ var id = reference.Uri.Substring(1);
- var idElement = signedXml.GetIdElement(_xmlDoc, id);
+ var idElement = signedXml.GetIdElement(_xmlDoc, id);
- if (idElement == _xmlDoc.DocumentElement)
- return true;
- else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element
- {
- var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
- if (assertionNode != idElement)
- return false;
- }
+ if (idElement == _xmlDoc.DocumentElement)
+ {
+ return true;
+ }
+ else // sometimes its not the "root" doc-element that is being signed, but the "assertion" element
+ {
+ var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
+ if (assertionNode != idElement)
+ {
+ return false;
+ }
+ }
- return true;
- }
+ return true;
+ }
- private bool IsExpired()
- {
- DateTime expirationDate = DateTime.MaxValue;
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
- if (node != null && node.Attributes["NotOnOrAfter"] != null)
- {
- DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
- }
- return DateTime.UtcNow > expirationDate.ToUniversalTime();
- }
+ private bool IsExpired()
+ {
+ var expirationDate = DateTime.MaxValue;
+ var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
+ if (node != null && node.Attributes["NotOnOrAfter"] != null)
+ {
+ DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
+ }
- public string GetNameID()
- {
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
- return node.InnerText;
- }
+ return DateTime.UtcNow > expirationDate.ToUniversalTime();
+ }
- public virtual string GetUpn()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn");
- }
+ public string GetNameID()
+ {
+ var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
+ return node.InnerText;
+ }
- public virtual string GetEmail()
- {
- return GetCustomAttribute("User.email")
- ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
- ?? GetCustomAttribute("mail"); //some providers put last name into an attribute named "mail"
- }
+ public virtual string GetUpn()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn");
+ }
- public virtual string GetFirstName()
- {
- return GetCustomAttribute("first_name")
- ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
- ?? GetCustomAttribute("User.FirstName")
- ?? GetCustomAttribute("givenName"); //some providers put last name into an attribute named "givenName"
- }
+ public virtual string GetEmail()
+ {
+ return GetCustomAttribute("User.email")
+ // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
+ ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")
+ // some providers put last name into an attribute named "mail"
+ ?? GetCustomAttribute("mail");
+ }
- public virtual string GetLastName()
- {
- return GetCustomAttribute("last_name")
- ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
- ?? GetCustomAttribute("User.LastName")
- ?? GetCustomAttribute("sn"); //some providers put last name into an attribute named "sn"
- }
+ public virtual string GetFirstName()
+ {
+ return GetCustomAttribute("first_name")
+ // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
+ ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")
+ ?? GetCustomAttribute("User.FirstName")
+ // some providers put last name into an attribute named "givenName"
+ ?? GetCustomAttribute("givenName");
+ }
- public virtual string GetDepartment()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
- ?? GetCustomAttribute("department");
- }
+ public virtual string GetLastName()
+ {
+ return GetCustomAttribute("last_name")
+ // some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
+ ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname")
+ ?? GetCustomAttribute("User.LastName")
+ // some providers put last name into an attribute named "sn"
+ ?? GetCustomAttribute("sn");
+ }
- public virtual string GetPhone()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone")
- ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
- }
+ public virtual string GetDepartment()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
+ ?? GetCustomAttribute("department");
+ }
- public virtual string GetCompany()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
- ?? GetCustomAttribute("organization")
- ?? GetCustomAttribute("User.CompanyName");
- }
+ public virtual string GetPhone()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone")
+ ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
+ }
- public virtual string GetLocation()
- {
- return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
- ?? GetCustomAttribute("physicalDeliveryOfficeName");
- }
+ public virtual string GetCompany()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
+ ?? GetCustomAttribute("organization")
+ ?? GetCustomAttribute("User.CompanyName");
+ }
- public string GetCustomAttribute(string attr)
- {
- XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
- return node == null ? null : node.InnerText;
- }
+ public virtual string GetLocation()
+ {
+ return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
+ ?? GetCustomAttribute("physicalDeliveryOfficeName");
+ }
- //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()
- {
- XmlNamespaceManager manager = new XmlNamespaceManager(_xmlDoc.NameTable);
- manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
- manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
- manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
+ public string GetCustomAttribute(string attr)
+ {
+ var node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
+ return node?.InnerText;
+ }
- return manager;
- }
- }
+ // 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()
+ {
+ var manager = new XmlNamespaceManager(_xmlDoc.NameTable);
+ manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
+ manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
+ manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
- public class AuthRequest
- {
- public string _id;
- private string _issue_instant;
-
- private string _issuer;
- private string _assertionConsumerServiceUrl;
-
- public enum AuthRequestFormat
- {
- Base64 = 1
- }
-
- public AuthRequest(string issuer, string assertionConsumerServiceUrl)
- {
- _id = "_" + Guid.NewGuid().ToString();
- _issue_instant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
-
- _issuer = issuer;
- _assertionConsumerServiceUrl = assertionConsumerServiceUrl;
- }
-
- public string GetRequest(AuthRequestFormat format)
- {
- using (StringWriter sw = new StringWriter())
- {
- XmlWriterSettings xws = new XmlWriterSettings();
- xws.OmitXmlDeclaration = true;
-
- using (XmlWriter xw = XmlWriter.Create(sw, xws))
- {
- xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
- xw.WriteAttributeString("ID", _id);
- xw.WriteAttributeString("Version", "2.0");
- xw.WriteAttributeString("IssueInstant", _issue_instant);
- xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
- xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl);
-
- xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
- xw.WriteString(_issuer);
- xw.WriteEndElement();
-
- xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol");
- xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
- xw.WriteAttributeString("AllowCreate", "true");
- xw.WriteEndElement();
-
- /*xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol");
- xw.WriteAttributeString("Comparison", "exact");
- xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion");
- xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
- xw.WriteEndElement();
- xw.WriteEndElement();*/
-
- xw.WriteEndElement();
- }
-
- if (format == AuthRequestFormat.Base64)
- {
- //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(sw.ToString());
- //return System.Convert.ToBase64String(toEncodeAsBytes);
-
- //https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E
- var memoryStream = new MemoryStream();
- var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false));
- writer.Write(sw.ToString());
- writer.Close();
- string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
- return result;
- }
-
- return null;
- }
- }
-
- //returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring
- public string GetRedirectUrl(string samlEndpoint, string relayState = null)
- {
- var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?";
-
- var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + HttpUtility.UrlEncode(GetRequest(AuthRequestFormat.Base64));
-
- if (!string.IsNullOrEmpty(relayState))
- {
- url += "&RelayState=" + HttpUtility.UrlEncode(relayState);
- }
-
- return url;
- }
- }
+ return manager;
+ }
+}
+
+public class AuthRequest
+{
+ private readonly string _id;
+ private readonly string _issueInstant;
+
+ private readonly string _issuer;
+ private readonly string _assertionConsumerServiceUrl;
+
+ public AuthRequest(string issuer, string assertionConsumerServiceUrl)
+ {
+ _id = "_" + Guid.NewGuid().ToString();
+ _issueInstant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
+
+ _issuer = issuer;
+ _assertionConsumerServiceUrl = assertionConsumerServiceUrl;
+ }
+
+ public enum AuthRequestFormat
+ {
+ ///
+ /// Base64 request.
+ ///
+ Base64 = 1
+ }
+
+ public string GetRequest(AuthRequestFormat format)
+ {
+ using var sw = new StringWriter();
+ var xws = new XmlWriterSettings();
+ xws.OmitXmlDeclaration = true;
+
+ using (var xw = XmlWriter.Create(sw, xws))
+ {
+ xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
+ xw.WriteAttributeString("ID", _id);
+ xw.WriteAttributeString("Version", "2.0");
+ xw.WriteAttributeString("IssueInstant", _issueInstant);
+ xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
+ xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl);
+
+ xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
+ xw.WriteString(_issuer);
+ xw.WriteEndElement();
+
+ xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol");
+ xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
+ xw.WriteAttributeString("AllowCreate", "true");
+ xw.WriteEndElement();
+
+ /*
+ xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol");
+ xw.WriteAttributeString("Comparison", "exact");
+ xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion");
+ xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
+ xw.WriteEndElement();
+ xw.WriteEndElement();
+ */
+
+ xw.WriteEndElement();
+ }
+
+ if (format == AuthRequestFormat.Base64)
+ {
+ // byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(sw.ToString());
+ // return System.Convert.ToBase64String(toEncodeAsBytes);
+
+ // https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E
+ var memoryStream = new MemoryStream();
+ var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false));
+ writer.Write(sw.ToString());
+ writer.Close();
+ var result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
+ return result;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring.
+ ///
+ /// The SAML endpoint.
+ /// The relay state.
+ /// The redirect url.
+ public string GetRedirectUrl(string samlEndpoint, string relayState = null)
+ {
+ var queryStringSeparator = samlEndpoint.Contains('?') ? "&" : "?";
+
+ var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + HttpUtility.UrlEncode(GetRequest(AuthRequestFormat.Base64));
+
+ if (!string.IsNullOrEmpty(relayState))
+ {
+ url += "&RelayState=" + HttpUtility.UrlEncode(relayState);
+ }
+
+ return url;
+ }
}
diff --git a/SSO-Auth/WebResponse.cs b/SSO-Auth/WebResponse.cs
index efce1d6..0b0e3bb 100644
--- a/SSO-Auth/WebResponse.cs
+++ b/SSO-Auth/WebResponse.cs
@@ -1,8 +1,8 @@
-namespace Jellyfin.Plugin.SSO_Auth
+namespace Jellyfin.Plugin.SSO_Auth;
+
+public static class WebResponse
{
- class WebResponse
- {
- public static string Base = @"
+ public static readonly string Base = @"
";
- }
- public static string SamlGenerator(string xml, string provider)
- {
- return Base + @"
+ }
+
+ public static string SamlGenerator(string xml, string provider)
+ {
+ return Base + @"
async function main() {
var xml = '" + xml + @"';
var deviceId = localStorage.getItem(""_deviceId2"");
@@ -481,6 +483,5 @@ document.addEventListener('DOMContentLoaded', function () {
});