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 MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Configuration;
|
namespace Moonlight.ApiServer.Configuration;
|
||||||
@@ -29,6 +30,10 @@ public record AppConfiguration
|
|||||||
Kestrel = new()
|
Kestrel = new()
|
||||||
{
|
{
|
||||||
AllowedOrigins = []
|
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")]
|
[YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")]
|
||||||
public bool FirstUserAdmin { get; set; } = true;
|
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
|
public record SessionsConfig
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Authentication;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Implementations.LocalAuth;
|
using Moonlight.ApiServer.Implementations.LocalAuth;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||||
@@ -13,13 +15,18 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
|||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
private readonly IAuthenticationSchemeProvider SchemeProvider;
|
||||||
|
private readonly IEnumerable<IAuthCheckExtension> Extensions;
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
// Add schemes which should be offered to the client here
|
public AuthController(
|
||||||
private readonly string[] SchemeWhitelist = [LocalAuthConstants.AuthenticationScheme];
|
IAuthenticationSchemeProvider schemeProvider,
|
||||||
|
IEnumerable<IAuthCheckExtension> extensions,
|
||||||
public AuthController(IAuthenticationSchemeProvider schemeProvider)
|
AppConfiguration configuration
|
||||||
|
)
|
||||||
{
|
{
|
||||||
SchemeProvider = schemeProvider;
|
SchemeProvider = schemeProvider;
|
||||||
|
Extensions = extensions;
|
||||||
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -27,8 +34,10 @@ public class AuthController : Controller
|
|||||||
{
|
{
|
||||||
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
var schemes = await SchemeProvider.GetAllSchemesAsync();
|
||||||
|
|
||||||
|
var allowedSchemes = Configuration.Authentication.EnabledSchemes;
|
||||||
|
|
||||||
return schemes
|
return schemes
|
||||||
.Where(x => SchemeWhitelist.Contains(x.Name))
|
.Where(x => allowedSchemes.Contains(x.Name))
|
||||||
.Select(scheme => new AuthSchemeResponse()
|
.Select(scheme => new AuthSchemeResponse()
|
||||||
{
|
{
|
||||||
DisplayName = scheme.DisplayName ?? scheme.Name,
|
DisplayName = scheme.DisplayName ?? scheme.Name,
|
||||||
@@ -40,11 +49,10 @@ public class AuthController : Controller
|
|||||||
[HttpGet("{identifier:alpha}")]
|
[HttpGet("{identifier:alpha}")]
|
||||||
public async Task StartScheme([FromRoute] string identifier)
|
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
|
if (!allowedSchemes.Contains(identifier))
|
||||||
// which isn't meant for users
|
|
||||||
if (scheme == null || !SchemeWhitelist.Contains(scheme.Name))
|
|
||||||
{
|
{
|
||||||
await Results
|
await Results
|
||||||
.Problem(
|
.Problem(
|
||||||
@@ -56,6 +64,22 @@ public class AuthController : Controller
|
|||||||
return;
|
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(
|
await HttpContext.ChallengeAsync(
|
||||||
scheme.Name,
|
scheme.Name,
|
||||||
new AuthenticationProperties()
|
new AuthenticationProperties()
|
||||||
@@ -67,7 +91,7 @@ public class AuthController : Controller
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
public Task<AuthClaimResponse[]> Check()
|
public async Task<AuthClaimResponse[]> Check()
|
||||||
{
|
{
|
||||||
var username = User.FindFirstValue(ClaimTypes.Name)!;
|
var username = User.FindFirstValue(ClaimTypes.Name)!;
|
||||||
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||||
@@ -75,6 +99,7 @@ public class AuthController : Controller
|
|||||||
var userId = User.FindFirstValue("UserId")!;
|
var userId = User.FindFirstValue("UserId")!;
|
||||||
var permissions = User.FindFirstValue("Permissions")!;
|
var permissions = User.FindFirstValue("Permissions")!;
|
||||||
|
|
||||||
|
// Create basic set of claims used by the frontend
|
||||||
var claims = new List<AuthClaimResponse>()
|
var claims = new List<AuthClaimResponse>()
|
||||||
{
|
{
|
||||||
new(ClaimTypes.Name, username),
|
new(ClaimTypes.Name, username),
|
||||||
@@ -84,11 +109,17 @@ public class AuthController : Controller
|
|||||||
new("Permissions", permissions)
|
new("Permissions", permissions)
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.FromResult(
|
// Enrich the frontend claims by extensions (used by plugins)
|
||||||
claims.ToArray()
|
foreach (var extension in Extensions)
|
||||||
|
{
|
||||||
|
claims.AddRange(
|
||||||
|
await extension.GetFrontendClaims(User)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return claims.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("logout")]
|
[HttpGet("logout")]
|
||||||
public async Task Logout()
|
public async Task 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 MoonCore.Helpers;
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.ApiServer.Interfaces;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Services;
|
namespace Moonlight.ApiServer.Services;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ public class UserAuthService
|
|||||||
private readonly ILogger<UserAuthService> Logger;
|
private readonly ILogger<UserAuthService> Logger;
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
|
private readonly IEnumerable<IUserAuthExtension> Extensions;
|
||||||
|
|
||||||
private const string UserIdClaim = "UserId";
|
private const string UserIdClaim = "UserId";
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
private const string IssuedAtClaim = "IssuedAt";
|
||||||
@@ -21,12 +23,14 @@ public class UserAuthService
|
|||||||
public UserAuthService(
|
public UserAuthService(
|
||||||
ILogger<UserAuthService> logger,
|
ILogger<UserAuthService> logger,
|
||||||
DatabaseRepository<User> userRepository,
|
DatabaseRepository<User> userRepository,
|
||||||
AppConfiguration configuration
|
AppConfiguration configuration,
|
||||||
|
IEnumerable<IUserAuthExtension> extensions
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
|
Extensions = extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Sync(ClaimsPrincipal? principal)
|
public async Task<bool> Sync(ClaimsPrincipal? principal)
|
||||||
@@ -93,12 +97,22 @@ public class UserAuthService
|
|||||||
await UserRepository.Update(user);
|
await UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich claims with required metadata
|
||||||
principal.Identities.First().AddClaims([
|
principal.Identities.First().AddClaims([
|
||||||
new Claim(UserIdClaim, user.Id.ToString()),
|
new Claim(UserIdClaim, user.Id.ToString()),
|
||||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||||
new Claim("Permissions", string.Join(';', user.Permissions))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +151,18 @@ public class UserAuthService
|
|||||||
// everything is fine. If not it means that the token should be invalidated
|
// everything is fine. If not it means that the token should be invalidated
|
||||||
// as it is too old
|
// 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">
|
<LazyLoader EnableDefaultSpacing="false" Load="Load">
|
||||||
@if (ShowSelection)
|
@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>
|
<p class="mb-4">Choose a login provider to start using the app</p>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user