This commit is contained in:
Sambhav Saggi
2022-04-16 13:39:25 -04:00
5 changed files with 111 additions and 27 deletions
+33
View File
@@ -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?
+20
View File
@@ -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.
+47 -26
View File
@@ -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.
/// </summary>
/// <param name="provider">The ID of the provider which will use the callback information.</param>
/// <param name="state">The current request state.</param>
/// <returns>A webpage that will complete the client-side flow.</returns>
// 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<string>(config.EnabledFolders);
StateManager[state].Folders = new List<string>(config.EnabledFolders);
}
else
{
StateManager[Request.Query["state"]].Folders = new List<string>();
StateManager[state].Folders = new List<string>();
}
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<List<string>>();
roles = (json[segments[^1]] as JArray).ToObject<List<string>>();
}
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");
}
/// <summary>
@@ -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
/// <param name="enableAllFolders">Determines whether all folders are enabled.</param>
/// <param name="enabledFolders">Determines which folders should be enabled for this client.</param>
/// <param name="authResponse">The client information to authenticate the user with.</param>
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
/// <param name="defaultProvider">The default provider of the user to be set after logging in.</param>
private async Task<AuthenticationResult> 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;
}
+10
View File
@@ -92,6 +92,11 @@ public class SamlConfig
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List<FolderRoleMap> FolderRoleMapping { get; set; }
/// <summary>
/// Gets or sets the default provider the user after logging in with SSO.
/// </summary>
public string DefaultProvider { get; set; }
}
/// <summary>
@@ -161,6 +166,11 @@ public class OidConfig
/// Gets or sets the claim to check roles against. Separated by "."s.
/// </summary>
public string RoleClaim { get; set; }
/// <summary>
/// Gets or sets the default provider the user after logging in with SSO.
/// </summary>
public string DefaultProvider { get; set; }
}
/// <summary>
+1 -1
View File
@@ -15,10 +15,10 @@
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.0" />
</ItemGroup>