forked from mirrors/jellyfin-plugin-sso
Merge branch 'main' of https://github.com/9p4/jellyfin-plugin-sso
This commit is contained in:
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user