diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..4f15148
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Versions (please complete the following information):**
+ - OS: [e.g. Linux]
+ - Browser: [e.g. chrome, safari]
+ - Jellyfin Version: [e.g. 10.8 Alpha 4]
+ - Plugin Version: [e.g. 2.0.1.0 or a Git tag]
+
+**Additional context**
+Add any other context about the problem here. Was the plugin built from source?
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..11fc491
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/SSO-Auth/Api/SSOController.cs b/SSO-Auth/Api/SSOController.cs
index 90aabfa..ad60764 100644
--- a/SSO-Auth/Api/SSOController.cs
+++ b/SSO-Auth/Api/SSOController.cs
@@ -12,6 +12,7 @@ using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -49,10 +50,13 @@ public class SSOController : ControllerBase
/// The GET endpoint for OpenID provider to callback to. Returns a webpage that parses client data and completes auth.
///
/// The ID of the provider which will use the callback information.
+ /// The current request state.
/// A webpage that will complete the client-side flow.
// Actually a GET: https://github.com/IdentityModel/IdentityModel.OidcClient/issues/325
[HttpGet("OID/r/{provider}")]
- public ActionResult OidPost(string provider) // Although this is a GET function, this function is called `Post` for consistency with SAML
+ public ActionResult OidPost(
+ [FromRoute] string provider,
+ [FromQuery] string state) // Although this is a GET function, this function is called `Post` for consistency with SAML
{
OidConfig config;
try
@@ -76,30 +80,30 @@ public class SSOController : ControllerBase
};
options.Policy.Discovery.ValidateEndpoints = false; // For Google and other providers with different endpoints
var oidcClient = new OidcClient(options);
- var state = StateManager[Request.Query["state"]].State;
- var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, state).Result;
+ var currentState = StateManager[state].State;
+ var result = oidcClient.ProcessResponseAsync(Request.QueryString.Value, currentState).Result;
if (result.IsError)
{
- return ReturnError(400, result.Error + " Try logging in again.");
+ return ReturnError(StatusCodes.Status400BadRequest, result.Error + " Try logging in again.");
}
if (!config.EnableFolderRoles)
{
- StateManager[Request.Query["state"]].Folders = new List(config.EnabledFolders);
+ StateManager[state].Folders = new List(config.EnabledFolders);
}
else
{
- StateManager[Request.Query["state"]].Folders = new List();
+ StateManager[state].Folders = new List();
}
foreach (var claim in result.User.Claims)
{
if (claim.Type == "preferred_username")
{
- StateManager[Request.Query["state"]].Username = claim.Value;
+ StateManager[state].Username = claim.Value;
if (config.Roles.Length == 0)
{
- StateManager[Request.Query["state"]].Valid = true;
+ StateManager[state].Valid = true;
}
}
@@ -132,7 +136,7 @@ public class SSOController : ControllerBase
}
// The final step is to take the JSON and turn it from a dictionary into a string
- roles = (json[segments[segments.Length - 1]] as JArray).ToObject>();
+ roles = (json[segments[^1]] as JArray).ToObject>();
}
foreach (string role in roles)
@@ -144,7 +148,7 @@ public class SSOController : ControllerBase
{
if (role.Equals(validRoles))
{
- StateManager[Request.Query["state"]].Valid = true;
+ StateManager[state].Valid = true;
}
}
}
@@ -156,7 +160,7 @@ public class SSOController : ControllerBase
{
if (role.Equals(validAdminRoles))
{
- StateManager[Request.Query["state"]].Admin = true;
+ StateManager[state].Admin = true;
}
}
}
@@ -168,7 +172,7 @@ public class SSOController : ControllerBase
{
if (role.Equals(folderRoleMap.Role))
{
- StateManager[Request.Query["state"]].Folders.AddRange(folderRoleMap.Folders);
+ StateManager[state].Folders.AddRange(folderRoleMap.Folders);
}
}
}
@@ -177,29 +181,34 @@ public class SSOController : ControllerBase
}
// If the provider doesn't support preferred_username, then use sub
- if (!StateManager[Request.Query["state"]].Valid)
+ if (!StateManager[state].Valid)
{
foreach (var claim in result.User.Claims)
{
if (claim.Type == "sub")
{
- StateManager[Request.Query["state"]].Username = claim.Value;
+ StateManager[state].Username = claim.Value;
if (config.Roles.Length == 0)
{
- StateManager[Request.Query["state"]].Valid = true;
+ StateManager[state].Valid = true;
}
}
}
}
- if (StateManager[Request.Query["state"]].Valid)
+ if (StateManager[state].Valid)
{
- return Content(WebResponse.Generator(data: Request.Query["state"], provider: provider, baseUrl: GetRequestBase(), mode: "OID"), MediaTypeNames.Text.Html);
+ return Content(WebResponse.Generator(data: state, provider: provider, baseUrl: GetRequestBase(), mode: "OID"), MediaTypeNames.Text.Html);
}
else
{
- _logger.LogWarning("OpenID user " + StateManager[Request.Query["state"]].Username + " has one or more incorrect role claims: " + string.Join(", ", result.User.Claims.Select(o => new { o.Type, o.Value })) + ". Expected any one of: " + string.Join(", ", config.Roles) + ".");
- return ReturnError(401, "Error. Check permissions.");
+ _logger.LogWarning(
+ "OpenID user {Username} has one or more incorrect role claims: {@Claims}. Expected any one of: {@ExpectedClaims}",
+ StateManager[state].Username,
+ result.User.Claims.Select(o => new { o.Type, o.Value }),
+ config.Roles);
+
+ return ReturnError(StatusCodes.Status401Unauthorized, "Error. Check permissions.");
}
}
@@ -322,7 +331,7 @@ public class SSOController : ControllerBase
{
if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid)
{
- var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), response)
+ var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), response, config.DefaultProvider)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -371,11 +380,15 @@ public class SSOController : ControllerBase
}
}
- _logger.LogWarning("SAML user " + samlResponse.GetNameID() + " has insufficient roles: " + string.Join(", ", samlResponse.GetCustomAttributes("Role")) + ". Expected any one of: " + string.Join(", ", config.Roles) + ".");
- return ReturnError(401, "Error. Check permissions.");
+ _logger.LogWarning(
+ "SAML user: {UserId} has insufficient roles: {@Roles}. Expected any one of: {@ExpectedRoles}",
+ samlResponse.GetNameID(),
+ samlResponse.GetCustomAttributes("Role"),
+ config.Roles);
+ return ReturnError(StatusCodes.Status401Unauthorized, "Error. Check permissions.");
}
- return ReturnError(400, "No active providers found");
+ return ReturnError(StatusCodes.Status400BadRequest, "No active providers found");
}
///
@@ -507,7 +520,7 @@ public class SSOController : ControllerBase
}
}
- var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), response)
+ var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), response, config.DefaultProvider)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -540,7 +553,8 @@ public class SSOController : ControllerBase
/// Determines whether all folders are enabled.
/// Determines which folders should be enabled for this client.
/// The client information to authenticate the user with.
- private async Task Authenticate(string username, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
+ /// The default provider of the user to be set after logging in.
+ private async Task Authenticate(string username, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse, string defaultProvider)
{
User user = null;
user = _userManager.GetUserByName(username);
@@ -572,6 +586,13 @@ public class SSOController : ControllerBase
authRequest.DeviceId = authResponse.DeviceID;
authRequest.DeviceName = authResponse.DeviceName;
_logger.LogInformation("Auth request created...");
+ if (!string.IsNullOrEmpty(defaultProvider))
+ {
+ user.AuthenticationProviderId = defaultProvider;
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ _logger.LogInformation("Set default login provider to " + defaultProvider);
+ }
+
return await _sessionManager.AuthenticateDirect(authRequest).ConfigureAwait(false);
}
@@ -596,7 +617,7 @@ public class SSOController : ControllerBase
{
var errorResult = new ContentResult();
errorResult.Content = message;
- errorResult.ContentType = "text/plain";
+ errorResult.ContentType = MediaTypeNames.Text.Plain;
errorResult.StatusCode = code;
return errorResult;
}
diff --git a/SSO-Auth/Config/PluginConfiguration.cs b/SSO-Auth/Config/PluginConfiguration.cs
index 2e4a874..ee42f41 100644
--- a/SSO-Auth/Config/PluginConfiguration.cs
+++ b/SSO-Auth/Config/PluginConfiguration.cs
@@ -92,6 +92,11 @@ public class SamlConfig
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List FolderRoleMapping { get; set; }
+
+ ///
+ /// Gets or sets the default provider the user after logging in with SSO.
+ ///
+ public string DefaultProvider { get; set; }
}
///
@@ -161,6 +166,11 @@ public class OidConfig
/// Gets or sets the claim to check roles against. Separated by "."s.
///
public string RoleClaim { get; set; }
+
+ ///
+ /// Gets or sets the default provider the user after logging in with SSO.
+ ///
+ public string DefaultProvider { get; set; }
}
///
diff --git a/SSO-Auth/SSO-Auth.csproj b/SSO-Auth/SSO-Auth.csproj
index ece422f..6eab6fe 100644
--- a/SSO-Auth/SSO-Auth.csproj
+++ b/SSO-Auth/SSO-Auth.csproj
@@ -15,10 +15,10 @@
+
-