Compare commits

...
Author SHA1 Message Date
Sambhav Saggi 330002282c Merge branch 'clean' of https://github.com/crobibero/jellyfin-plugin-sso into crobibero-clean 2022-01-17 13:25:43 -05:00
Sambhav Saggi 2f828f1210 Base URL patch 2022-01-17 13:23:50 -05:00
Cody Robibero d05af0cfa9 Remove all warnings
Signed-off-by: Cody Robibero <[email protected]>
2022-01-17 08:12:31 -07:00
Sambhav Saggi a23b00f02f Update README.md 2022-01-17 00:05:30 -05:00
Sambhav Saggi 144a0223ca Update build to include link to repo 2022-01-16 23:55:06 -05:00
8 changed files with 633 additions and 564 deletions
+4
View File
@@ -19,6 +19,10 @@ There is NO admin configuration! You must use the API to configure the program!
This is my first time writing C# so please take all of the code written here with a grain of salt. This program should be reasonably secure since it validates all information passed from the client with either a certificate or a secret internal state.
## Installing
Add the package repo [https://repo.saggis.com/jellyfin/manifest.json](https://repo.saggis.com/jellyfin/manifest.json) to your Jellyfin configuration. Then, install the package!
## Building
This is built with .NET 6.0. Build with `dotnet publish .` for the debug release in the `SSO-Auth` directory. Copy over the `IdentityModel.OidcClient.dll` and the `SSO-Auth.dll` files in the `/bin/Debug/net6.0/publish` directory to a new folder in your Jellyfin configuration: `config/plugins/sso`.
+289 -260
View File
@@ -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;
/// <summary>
/// The sso api controller.
/// </summary>
[ApiController]
[Route("[controller]")]
public class SSOController : ControllerBase
{
private readonly IUserManager _userManager;
private readonly ISessionManager _sessionManager;
private readonly ILogger<SSOController> _logger;
private static readonly IDictionary<string, TimedAuthorizeState> StateManager = new Dictionary<string, TimedAuthorizeState>();
/// <summary>
/// The sso api controller.
/// Initializes a new instance of the <see cref="SSOController"/> class.
/// </summary>
[ApiController]
[Route("[controller]")]
public class SSOController : ControllerBase
/// <param name="logger">Instance of the <see cref="ILogger{SSOController}"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public SSOController(ILogger<SSOController> logger, ISessionManager sessionManager, IUserManager userManager)
{
private readonly IApplicationHost _applicationHost;
private readonly ISessionManager _sessionManager;
private readonly ILogger<SSOController> _logger;
private static IDictionary<string, TimedAuthorizeState> _stateManager = new Dictionary<string, TimedAuthorizeState>();
_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<SSOController>();
_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<ActionResult> 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<ActionResult> 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<string, TimedAuthorizeState> 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<ActionResult> 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<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
return Problem("Something went wrong");
}
private async Task<AuthenticationResult> 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<IUserManager>();
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<string, TimedAuthorizeState> 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; }
}
+45 -43
View File
@@ -1,57 +1,59 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace Jellyfin.Plugin.SSO_Auth.Config {
namespace Jellyfin.Plugin.SSO_Auth.Config;
/// <summary>
/// Plugin Configuration.
/// </summary>
public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration
{
/// <summary>
/// Plugin Configuration.
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
/// </summary>
public class PluginConfiguration : MediaBrowser.Model.Plugins.BasePluginConfiguration {
/// <summary>
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
/// </summary>
public PluginConfiguration()
{
SamlConfigs = new List<SamlConfig>();
OIDConfigs = new List<OIDConfig>();
}
[XmlArray("SamlConfigs"), XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
public List<SamlConfig> SamlConfigs { get; set; }
[XmlArray("OIDConfigs"), XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
public List<OIDConfig> 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<SamlConfig>();
OIDConfigs = new List<OIDConfig>();
}
[XmlRoot("PluginConfiguration")]
public class OIDConfig
{
public string OIDEndpoint { get; set; }
[XmlArray("SamlConfigs")]
[XmlArrayItem(typeof(SamlConfig), ElementName = "SamlConfigs")]
public List<SamlConfig> SamlConfigs { get; set; }
public string OIDClientId { get; set; }
[XmlArray("OIDConfigs")]
[XmlArrayItem(typeof(OIDConfig), ElementName = "OIDConfigs")]
public List<OIDConfig> OIDConfigs { get; set; }
}
public string OIDSecret { get; set; }
[XmlRoot("PluginConfiguration")]
public class SamlConfig
{
public string SamlEndpoint { get; set; }
public bool Enabled { get; set; }
public string SamlClientId { get; set; }
public bool EnableAllFolders { get; set; }
public string SamlCertificate { get; set; }
public string[] EnabledFolders { 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; }
}
+1 -1
View File
@@ -24,7 +24,7 @@
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
+15 -12
View File
@@ -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<PluginConfiguration>, IHasWebPages {
public SSOPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) {
Instance = this;
namespace Jellyfin.Plugin.SSO_Auth;
public class SSOPlugin : BasePlugin<PluginConfiguration>, 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<PluginPageInfo> GetPages() {
return new[] {
new PluginPageInfo {
Name = Name,
EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
}
};
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = $"{GetType().Namespace}.Config.configPage.html"
};
}
}
}
+260 -232
View File
@@ -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
{
/// <summary>
/// Base64 request.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="samlEndpoint">The SAML endpoint.</param>
/// <param name="relayState">The relay state.</param>
/// <returns>The redirect url.</returns>
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;
}
}
+15 -14
View File
@@ -1,8 +1,8 @@
namespace Jellyfin.Plugin.SSO_Auth
namespace Jellyfin.Plugin.SSO_Auth;
public static class WebResponse
{
class WebResponse
{
public static string Base = @"<!DOCTYPE html>
public static readonly string Base = @"<!DOCTYPE html>
<html><head></head><body><script>
function isTv() {
// This is going to be really difficult to get right
@@ -390,9 +390,10 @@ function getDeviceName() {
}
";
public static string OIDGenerator(string data, string provider)
{
return Base + @"
public static string OIDGenerator(string data, string provider, string baseUrl)
{
return Base + @"
async function main() {
var data = '" + data + @"';
var deviceId = localStorage.getItem(""_deviceId2"");
@@ -403,7 +404,7 @@ async function main() {
var request = {'deviceID': deviceId, 'appName': appName, 'appVersion': appVersion, deviceName: 'deviceName', data: data, provider: '" + provider + @"'};
var url = '/sso/OID/Auth';
var url = '" + baseUrl + @"/sso/OID/Auth';
let response = await new Promise(resolve => {
var xhr = new XMLHttpRequest();
@@ -435,10 +436,11 @@ document.addEventListener('DOMContentLoaded', function () {
});
</script></body></html>";
}
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 () {
});
</script></body></html>";
}
}
}
+4 -2
View File
@@ -5,8 +5,10 @@ version: "1"
targetAbi: "10.8.0.0"
framework: "net6.0"
owner: "sambhavsaggi"
overview: "Authenticate users against an SSO provider"
description: This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
overview: "Authenticate users against an SSO provider."
description: |
This plugin allows users to sign in through an SSO provider (such as Google, Facebook, or your own provider). This enables one-click signin.
Review documentation at https://github.com/sambhavsaggi/jellyfin-plugin-sso
category: "Authentication"
artifacts:
- "SSO-Auth.dll"