Added extendability to the sign-in / sync, the session validation and the frontend claims transfer calls
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Moonlight.ApiServer.Configuration;
|
||||
@@ -29,6 +30,10 @@ public record AppConfiguration
|
||||
Kestrel = new()
|
||||
{
|
||||
AllowedOrigins = []
|
||||
},
|
||||
Authentication = new()
|
||||
{
|
||||
EnabledSchemes = []
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -60,6 +65,9 @@ public record AppConfiguration
|
||||
|
||||
[YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
|
||||
public bool FirstUserAdmin { get; set; } = true;
|
||||
|
||||
[YamlMember(Description = "This specifies the authentication schemes the frontend should be able to challenge")]
|
||||
public string[] EnabledSchemes { get; set; } = [LocalAuthConstants.AuthenticationScheme];
|
||||
}
|
||||
|
||||
public record SessionsConfig
|
||||
|
||||
@@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||
using Moonlight.ApiServer.Interfaces;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||
@@ -13,13 +15,18 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
||||
private readonly IEnumerable<IAuthCheckExtension> Extensions;
|
||||
private readonly AppConfiguration Configuration;
|
||||
|
||||
// Add schemes which should be offered to the client here
|
||||
private readonly string[] SchemeWhitelist = [LocalAuthConstants.AuthenticationScheme];
|
||||
|
||||
public AuthController(IAuthenticationSchemeProvider schemeProvider)
|
||||
public AuthController(
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IEnumerable<IAuthCheckExtension> extensions,
|
||||
AppConfiguration configuration
|
||||
)
|
||||
{
|
||||
SchemeProvider = schemeProvider;
|
||||
Extensions = extensions;
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -27,8 +34,10 @@ public class AuthController : Controller
|
||||
{
|
||||
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
||||
|
||||
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
||||
|
||||
return schemes
|
||||
.Where(x => SchemeWhitelist.Contains(x.Name))
|
||||
.Where(x => allowedSchemes.Contains(x.Name))
|
||||
.Select(scheme => new AuthSchemeResponse()
|
||||
{
|
||||
DisplayName = scheme.DisplayName ?? scheme.Name,
|
||||
@@ -40,11 +49,10 @@ public class AuthController : Controller
|
||||
[HttpGet("{identifier:alpha}")]
|
||||
public async Task StartScheme([FromRoute] string identifier)
|
||||
{
|
||||
var scheme = await SchemeProvider.GetSchemeAsync(identifier);
|
||||
// Validate identifier against our enable list
|
||||
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
||||
|
||||
// The check for the whitelist ensures a user isn't starting an auth flow
|
||||
// which isn't meant for users
|
||||
if (scheme == null || !SchemeWhitelist.Contains(scheme.Name))
|
||||
if (!allowedSchemes.Contains(identifier))
|
||||
{
|
||||
await Results
|
||||
.Problem(
|
||||
@@ -56,6 +64,22 @@ public class AuthController : Controller
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we can check if it even exists
|
||||
var scheme = await SchemeProvider.GetSchemeAsync(identifier);
|
||||
|
||||
if (scheme == null)
|
||||
{
|
||||
await Results
|
||||
.Problem(
|
||||
"Invalid scheme identifier provided",
|
||||
statusCode: 404
|
||||
)
|
||||
.ExecuteAsync(HttpContext);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything fine, challenge the frontend
|
||||
await HttpContext.ChallengeAsync(
|
||||
scheme.Name,
|
||||
new AuthenticationProperties()
|
||||
@@ -67,7 +91,7 @@ public class AuthController : Controller
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("check")]
|
||||
public Task<AuthClaimResponse[]> Check()
|
||||
public async Task<AuthClaimResponse[]> Check()
|
||||
{
|
||||
var username = User.FindFirstValue(ClaimTypes.Name)!;
|
||||
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||
@@ -75,6 +99,7 @@ public class AuthController : Controller
|
||||
var userId = User.FindFirstValue("UserId")!;
|
||||
var permissions = User.FindFirstValue("Permissions")!;
|
||||
|
||||
// Create basic set of claims used by the frontend
|
||||
var claims = new List<AuthClaimResponse>()
|
||||
{
|
||||
new(ClaimTypes.Name, username),
|
||||
@@ -84,9 +109,15 @@ public class AuthController : Controller
|
||||
new("Permissions", permissions)
|
||||
};
|
||||
|
||||
return Task.FromResult(
|
||||
claims.ToArray()
|
||||
);
|
||||
// Enrich the frontend claims by extensions (used by plugins)
|
||||
foreach (var extension in Extensions)
|
||||
{
|
||||
claims.AddRange(
|
||||
await extension.GetFrontendClaims(User)
|
||||
);
|
||||
}
|
||||
|
||||
return claims.ToArray();
|
||||
}
|
||||
|
||||
[HttpGet("logout")]
|
||||
|
||||
16
Moonlight.ApiServer/Interfaces/IAuthCheckExtension.cs
Normal file
16
Moonlight.ApiServer/Interfaces/IAuthCheckExtension.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Security.Claims;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.ApiServer.Interfaces;
|
||||
|
||||
public interface IAuthCheckExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// This function will be called by the frontend reaching out to the api server for claim information.
|
||||
/// You can use this function to give your frontend plugins access to user specific data which is
|
||||
/// static for the session. E.g. the avatar url of a user
|
||||
/// </summary>
|
||||
/// <param name="principal">The principal of the current signed-in user</param>
|
||||
/// <returns>An array of claim responses which gets added to the list of claims to send to the frontend</returns>
|
||||
public Task<AuthClaimResponse[]> GetFrontendClaims(ClaimsPrincipal principal);
|
||||
}
|
||||
25
Moonlight.ApiServer/Interfaces/IUserAuthExtension.cs
Normal file
25
Moonlight.ApiServer/Interfaces/IUserAuthExtension.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Security.Claims;
|
||||
using Moonlight.ApiServer.Database.Entities;
|
||||
|
||||
namespace Moonlight.ApiServer.Interfaces;
|
||||
|
||||
public interface IUserAuthExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// This function is called on every sign-in. It should be used to synchronize additional user data from the principal
|
||||
/// or extend the claims saved in the user session
|
||||
/// </summary>
|
||||
/// <param name="user">The current user this method is called for</param>
|
||||
/// <param name="principal">The principal after being processed by moonlight itself</param>
|
||||
/// <returns>The result of the synchronisation. Returning false will immediately invalidate the sign-in and no other extensions will be called</returns>
|
||||
public Task<bool> Sync(User user, ClaimsPrincipal principal);
|
||||
|
||||
/// <summary>
|
||||
/// IMPORTANT: Please note that heavy operations should not occur in this method as it will be called for every request
|
||||
/// of every user
|
||||
/// </summary>
|
||||
/// <param name="user">The current user this method is called for</param>
|
||||
/// <param name="principal">The principal after being processed by moonlight itself</param>
|
||||
/// <returns>The result of the validation. Returning false will immediately invalidate the users session and no other extensions will be called</returns>
|
||||
public Task<bool> Validate(User user, ClaimsPrincipal principal);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using MoonCore.Extended.Helpers;
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database.Entities;
|
||||
using Moonlight.ApiServer.Interfaces;
|
||||
|
||||
namespace Moonlight.ApiServer.Services;
|
||||
|
||||
@@ -14,6 +15,7 @@ public class UserAuthService
|
||||
private readonly ILogger<UserAuthService> Logger;
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly AppConfiguration Configuration;
|
||||
private readonly IEnumerable<IUserAuthExtension> Extensions;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
@@ -21,12 +23,14 @@ public class UserAuthService
|
||||
public UserAuthService(
|
||||
ILogger<UserAuthService> logger,
|
||||
DatabaseRepository<User> userRepository,
|
||||
AppConfiguration configuration
|
||||
AppConfiguration configuration,
|
||||
IEnumerable<IUserAuthExtension> extensions
|
||||
)
|
||||
{
|
||||
Logger = logger;
|
||||
UserRepository = userRepository;
|
||||
Configuration = configuration;
|
||||
Extensions = extensions;
|
||||
}
|
||||
|
||||
public async Task<bool> Sync(ClaimsPrincipal? principal)
|
||||
@@ -93,12 +97,22 @@ public class UserAuthService
|
||||
await UserRepository.Update(user);
|
||||
}
|
||||
|
||||
// Enrich claims with required metadata
|
||||
principal.Identities.First().AddClaims([
|
||||
new Claim(UserIdClaim, user.Id.ToString()),
|
||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||
new Claim("Permissions", string.Join(';', user.Permissions))
|
||||
]);
|
||||
|
||||
// Call extensions
|
||||
foreach (var extension in Extensions)
|
||||
{
|
||||
var result = await extension.Sync(user, principal);
|
||||
|
||||
if (!result) // Exit immediately if result is false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -137,6 +151,18 @@ public class UserAuthService
|
||||
// everything is fine. If not it means that the token should be invalidated
|
||||
// as it is too old
|
||||
|
||||
return issuedAt > user.TokenValidTimestamp;
|
||||
if (issuedAt < user.TokenValidTimestamp)
|
||||
return false;
|
||||
|
||||
// Call extensions
|
||||
foreach (var extension in Extensions)
|
||||
{
|
||||
var result = await extension.Validate(user, principal);
|
||||
|
||||
if (!result) // Exit immediately if result is false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<LazyLoader EnableDefaultSpacing="false" Load="Load">
|
||||
@if (ShowSelection)
|
||||
{
|
||||
<h5 class="card-title mb-2.5">Login to MoonCore</h5>
|
||||
<h5 class="card-title mb-2.5">Login to Moonlight</h5>
|
||||
|
||||
<p class="mb-4">Choose a login provider to start using the app</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user