From d05af0cfa92de2d3f78a5dcf9da0d69a85b5037f Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 17 Jan 2022 08:06:24 -0700 Subject: [PATCH 1/2] Remove all warnings Signed-off-by: Cody Robibero --- SSO-Auth/Api/SSOController.cs | 549 +++++++++++++------------ SSO-Auth/Config/PluginConfiguration.cs | 96 ++--- SSO-Auth/SSO-Auth.csproj | 2 +- SSO-Auth/SSOPlugin.cs | 27 +- SSO-Auth/Saml.cs | 492 +++++++++++----------- SSO-Auth/WebResponse.cs | 25 +- 6 files changed, 627 insertions(+), 564 deletions(-) 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 () { }); "; - } } } From 2f828f1210f5410d48c8d05f437a2acba60406d7 Mon Sep 17 00:00:00 2001 From: Sambhav Saggi <17993169+sambhavsaggi@users.noreply.github.com> Date: Mon, 17 Jan 2022 13:23:50 -0500 Subject: [PATCH 2/2] Base URL patch --- SSO-Auth/Api/SSOController.cs | 549 ++++++++++++++++++---------------- SSO-Auth/WebResponse.cs | 29 +- 2 files changed, 304 insertions(+), 274 deletions(-) diff --git a/SSO-Auth/Api/SSOController.cs b/SSO-Auth/Api/SSOController.cs index a5edebf..6dda3b0 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, 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) + 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, baseUrl: GetRequestBase()), 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/WebResponse.cs b/SSO-Auth/WebResponse.cs index efce1d6..9fc7981 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, string baseUrl) + { + return Base + @" async function main() { var xml = '" + xml + @"'; var deviceId = localStorage.getItem(""_deviceId2""); @@ -449,7 +451,7 @@ async function main() { var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: xml, provider: '" + provider + @"'}; - var url = '/sso/SAML/Auth'; + var url = '" + baseUrl + @"/sso/SAML/Auth'; let response = await new Promise(resolve => { var xhr = new XMLHttpRequest(); @@ -481,6 +483,5 @@ document.addEventListener('DOMContentLoaded', function () { }); "; - } } }