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 @@ + -