From fce44f49b664694ebe66e4e72cb6392c462b2345 Mon Sep 17 00:00:00 2001 From: Masu-Baumgartner <68913099+Masu-Baumgartner@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:34:19 +0100 Subject: [PATCH] Implemented apikey backend --- .../Authentication/PermClaimsPrinciple.cs | 53 ------ .../Admin/ApiKeys/ApiKeysController.cs | 2 +- .../Admin/Users/UsersController.cs | 2 +- .../Http/Controllers/Auth/AuthController.cs | 11 +- .../Middleware/ApiAuthenticationMiddleware.cs | 64 +++++++ .../Middleware/AuthenticationMiddleware.cs | 176 ------------------ .../Middleware/AuthorizationMiddleware.cs | 7 +- .../Interfaces/Startup/IDatabaseStartup.cs | 1 - .../Moonlight.ApiServer.csproj | 2 +- Moonlight.ApiServer/Program.cs | 2 + Moonlight.ApiServer/Startup.cs | 7 +- Moonlight.Client/Interfaces/IAppLoader.cs | 2 - Moonlight.Client/Moonlight.Client.csproj | 4 +- Moonlight.Client/Program.cs | 2 - Moonlight.Client/Services/IdentityService.cs | 3 - Moonlight.Client/UI/Layouts/MainLayout.razor | 3 - Moonlight.Client/UI/Partials/AppSidebar.razor | 3 +- .../UI/Views/Admin/Api/Index.razor | 2 +- .../UI/Views/Admin/Sys/Index.razor | 2 +- .../UI/Views/Admin/Users/Index.razor | 2 +- 20 files changed, 90 insertions(+), 260 deletions(-) delete mode 100644 Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs create mode 100644 Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs delete mode 100644 Moonlight.ApiServer/Http/Middleware/AuthenticationMiddleware.cs diff --git a/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs b/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs deleted file mode 100644 index 833e0e4f..00000000 --- a/Moonlight.ApiServer/Helpers/Authentication/PermClaimsPrinciple.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Security.Claims; -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Helpers.Authentication; - -public class PermClaimsPrinciple : ClaimsPrincipal -{ - public string[] Permissions { get; private set; } - public User? CurrentModelNullable { get; private set; } - public User CurrentModel => CurrentModelNullable!; - - public PermClaimsPrinciple(string[] permissions, User? currentModelNullable) - { - Permissions = permissions; - CurrentModelNullable = currentModelNullable; - } - - public bool HasPermission(string requiredPermission) - { - // Check for wildcard permission - if (Permissions.Contains("*")) - return true; - - var requiredSegments = requiredPermission.Split('.'); - - // Check if the user has the exact permission or a wildcard match - foreach (var permission in Permissions) - { - var permissionSegments = permission.Split('.'); - - // Iterate over the segments of the required permission - for (var i = 0; i < requiredSegments.Length; i++) - { - // If the current segment matches or is a wildcard, continue to the next segment - if (i < permissionSegments.Length && requiredSegments[i] == permissionSegments[i] || - permissionSegments[i] == "*") - { - // If we've reached the end of the permissionSegments array, it means we've found a match - if (i == permissionSegments.Length - 1) - return true; // Found an exact match or a wildcard match - } - else - { - // If we reach here, it means the segments don't match and we break out of the loop - break; - } - } - } - - // No matching permission found - return false; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs index ad565abd..ead47432 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using MoonCore.Blazor.Tailwind.Attributes; +using MoonCore.Attributes; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; using MoonCore.Helpers; diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs index 0527f1e2..f24165d1 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/Users/UsersController.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using MoonCore.Blazor.Tailwind.Attributes; +using MoonCore.Attributes; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index 58734334..302a795c 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,15 +1,16 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc; -using MoonCore.Blazor.Tailwind.Attributes; +using MoonCore.Attributes; +using MoonCore.Authentication; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; using MoonCore.Extended.OAuth2.ApiServer; +using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.Services; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; -using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Interfaces.Auth; using Moonlight.ApiServer.Interfaces.OAuth2; using Moonlight.Shared.Http.Requests.Auth; @@ -210,14 +211,14 @@ public class AuthController : Controller [RequirePermission("meta.authenticated")] public Task Check() { - var perm = HttpContext.User as PermClaimsPrinciple; - var user = perm!.CurrentModel; + var permClaim = (HttpContext.User as PermClaimsPrinciple)!; + var user = (User)permClaim.IdentityModel; var response = new CheckResponse() { Email = user.Email, Username = user.Username, - Permissions = perm.Permissions + Permissions = permClaim.Permissions }; return Task.FromResult(response); diff --git a/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs new file mode 100644 index 00000000..34bed758 --- /dev/null +++ b/Moonlight.ApiServer/Http/Middleware/ApiAuthenticationMiddleware.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using MoonCore.Authentication; +using MoonCore.Extended.Abstractions; +using Moonlight.ApiServer.Database.Entities; + +namespace Moonlight.ApiServer.Http.Middleware; + +public class ApiAuthenticationMiddleware +{ + private readonly RequestDelegate Next; + private readonly ILogger Logger; + + public ApiAuthenticationMiddleware(RequestDelegate next, ILogger logger) + { + Next = next; + Logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + await Authenticate(context); + await Next(context); + } + + public Task Authenticate(HttpContext context) + { + var request = context.Request; + + if(!request.Headers.ContainsKey("Authorization")) + return Task.CompletedTask; + + if(request.Headers["Authorization"].Count == 0) + return Task.CompletedTask; + + var authHeader = request.Headers["Authorization"].First(); + + if(string.IsNullOrEmpty(authHeader)) + return Task.CompletedTask; + + var parts = authHeader.Split(" "); + + if(parts.Length != 2) + return Task.CompletedTask; + + var bearerValue = parts[1]; + + if(!bearerValue.StartsWith("api_")) + return Task.CompletedTask; + + if(bearerValue.Length != "api_".Length + 32) + return Task.CompletedTask; + + var apiKeyRepo = context.RequestServices.GetRequiredService>(); + var apiKey = apiKeyRepo.Get().FirstOrDefault(x => x.Secret == bearerValue); + + if(apiKey == null) + return Task.CompletedTask; + + var permissions = JsonSerializer.Deserialize(apiKey.PermissionsJson) ?? []; + context.User = new PermClaimsPrinciple(permissions); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/AuthenticationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthenticationMiddleware.cs deleted file mode 100644 index 0960818e..00000000 --- a/Moonlight.ApiServer/Http/Middleware/AuthenticationMiddleware.cs +++ /dev/null @@ -1,176 +0,0 @@ -namespace Moonlight.ApiServer.Http.Middleware; - -public class AuthenticationMiddleware -{ - private readonly RequestDelegate Next; - private readonly ILogger Logger; - - public AuthenticationMiddleware(RequestDelegate next, ILogger logger) - { - Next = next; - Logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - //await Authenticate(context); - await Next(context); - } -/* - private async Task Authenticate(HttpContext context) - { - var request = context.Request; - - if (!request.Cookies.TryGetValue("ml-access", out var accessToken) || - !request.Cookies.TryGetValue("ml-refresh", out var refreshToken)) - return; - - if(string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) - return; - - // TODO: Validate if both are valid jwts (maybe) - - // - var tokenHelper = context.RequestServices.GetRequiredService(); - var configService = context.RequestServices.GetRequiredService>(); - - User? user = null; - - if (!await tokenHelper.IsValidAccessToken(accessToken, configService.Get().Authentication.MlAccessSecret, - data => - { - if (!data.TryGetValue("userId", out var userIdStr)) - return false; - - if (!int.TryParse(userIdStr, out var userId)) - return false; - - var userRepo = context.RequestServices.GetRequiredService>(); - - user = userRepo.Get().FirstOrDefault(x => x.Id == userId); - - return user != null; - })) - { - return; - } - - if(user == null) - return; - - // Validate external access - if (DateTime.UtcNow > user.RefreshTimestamp) - { - var tokenConsumer = new TokenConsumer(user.AccessToken, user.RefreshToken, user.RefreshTimestamp, - async refreshToken => - { - var oauth2Service = context.RequestServices.GetRequiredService(); - - var accessData = await oauth2Service.RefreshAccess(refreshToken); - - user.AccessToken = accessData.AccessToken; - user.RefreshToken = accessData.RefreshToken; - user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn); - - var userRepo = context.RequestServices.GetRequiredService>(); - - userRepo.Update(user); - - return new TokenPair() - { - AccessToken = user.AccessToken, - RefreshToken = user.RefreshToken - }; - }); - - await tokenConsumer.GetAccessToken(); - //TODO: API CALL - } - - // Load permissions, handle empty values - var permissions = JsonSerializer.Deserialize( - string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson - ) ?? []; - - // Save permission state - context.User = new PermClaimsPrinciple(permissions, user); - - /// IGNORE - string? token = null; - - // Cookie for Moonlight.Client - if (request.Cookies.ContainsKey("token") && !string.IsNullOrEmpty(request.Cookies["token"])) - token = request.Cookies["token"]; - - // Header for api clients - if (request.Headers.ContainsKey("Authorization") && !string.IsNullOrEmpty(request.Cookies["Authorization"])) - { - var headerValue = request.Cookies["Authorization"] ?? ""; - - if (headerValue.StartsWith("Bearer")) - { - var headerParts = headerValue.Split(" "); - - if (headerParts.Length > 1 && !string.IsNullOrEmpty(headerParts[1])) - token = headerParts[1]; - } - } - - if(token == null) - return; - - // Validate token - if (token.Length > 300) - { - Logger.LogWarning("Received token bigger than 300 characters, Length: {length}", token.Length); - return; - } - - // Decide which authentication method we need for the token - if (token.Count(x => x == '.') == 2) // JWT only has two dots - await AuthenticateUser(context, token); - else - await AuthenticateApiKey(context, token); - } - - private async Task AuthenticateUser(HttpContext context, string jwt) - { - var jwtHelper = context.RequestServices.GetRequiredService(); - var configService = context.RequestServices.GetRequiredService>(); - var secret = configService.Get().Authentication.Secret; - - if (!await jwtHelper.Validate(secret, jwt, "login")) - return; - - var data = await jwtHelper.Decode(secret, jwt); - - if (!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString)) - return; - - var userId = int.Parse(userIdString); - var issuedAt = DateTimeOffset.FromUnixTimeSeconds(long.Parse(issuedAtString)).DateTime; - - var userRepo = context.RequestServices.GetRequiredService>(); - var user = userRepo.Get().FirstOrDefault(x => x.Id == userId); - - if (user == null) - return; - - // Check if token is in the past - if (user.TokenValidTimestamp > issuedAt) - return; - - // Load permissions, handle empty values - var permissions = JsonSerializer.Deserialize( - string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson - ) ?? []; - - // Save permission state - context.User = new PermClaimsPrinciple(permissions, user); - } - - private async Task AuthenticateApiKey(HttpContext context, string apiKey) - { - } -*/ -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs index cd238b79..323bf7bf 100644 --- a/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs +++ b/Moonlight.ApiServer/Http/Middleware/AuthorizationMiddleware.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc.Controllers; -using MoonCore.Blazor.Tailwind.Attributes; +using MoonCore.Attributes; +using MoonCore.Authentication; +using MoonCore.Extensions; using Moonlight.ApiServer.Exceptions; -using Moonlight.ApiServer.Helpers.Authentication; namespace Moonlight.ApiServer.Http.Middleware; @@ -64,7 +65,7 @@ public class AuthorizationMiddleware } // Check if one of the required permissions is to be logged in - if (requiredPermissions.Any(x => x == "meta.authenticated") && permClaimsPrinciple.CurrentModelNullable == null) + if (requiredPermissions.Any(x => x == "meta.authenticated") && permClaimsPrinciple.IdentityModel == null) { await Results.Problem( title: "This endpoint requires a user authenticated token", diff --git a/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs index d8c9a952..676392bf 100644 --- a/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs +++ b/Moonlight.ApiServer/Interfaces/Startup/IDatabaseStartup.cs @@ -1,5 +1,4 @@ using Moonlight.ApiServer.Helpers; -using Moonlight.ApiServer.Models; namespace Moonlight.ApiServer.Interfaces.Startup; diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index b55cbc6b..39105cf8 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Moonlight.ApiServer/Program.cs b/Moonlight.ApiServer/Program.cs index 0c71fa86..330d0249 100644 --- a/Moonlight.ApiServer/Program.cs +++ b/Moonlight.ApiServer/Program.cs @@ -158,6 +158,8 @@ foreach (var startupInterface in appStartupInterfaces) } } +app.UseMiddleware(); + app.UseMiddleware(); // Call interfaces diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 4f22a96e..de29b816 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using MoonCore.Authentication; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Extensions; using MoonCore.Extended.Helpers; @@ -7,7 +8,6 @@ using MoonCore.Helpers; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; -using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Interfaces.Startup; namespace Moonlight.ApiServer; @@ -79,7 +79,10 @@ public static class Startup ) ?? []; // Save permission state - context.User = new PermClaimsPrinciple(permissions, user); + context.User = new PermClaimsPrinciple(permissions) + { + IdentityModel = user + }; return true; }; diff --git a/Moonlight.Client/Interfaces/IAppLoader.cs b/Moonlight.Client/Interfaces/IAppLoader.cs index 4cc8c280..f1e2b5e0 100644 --- a/Moonlight.Client/Interfaces/IAppLoader.cs +++ b/Moonlight.Client/Interfaces/IAppLoader.cs @@ -1,5 +1,3 @@ -using Moonlight.Client.UI.Layouts; - namespace Moonlight.Client.Interfaces; public interface IAppLoader diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index 4b638f22..9bb5a597 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/Moonlight.Client/Program.cs b/Moonlight.Client/Program.cs index 4a20b3c5..cd4229e8 100644 --- a/Moonlight.Client/Program.cs +++ b/Moonlight.Client/Program.cs @@ -11,8 +11,6 @@ using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.Models; using MoonCore.PluginFramework.Extensions; -using MoonCore.PluginFramework.Services; -using Moonlight.Client.Implementations; using Moonlight.Client.Interfaces; using Moonlight.Client.Services; using Moonlight.Client.UI; diff --git a/Moonlight.Client/Services/IdentityService.cs b/Moonlight.Client/Services/IdentityService.cs index 68489caa..f5d70ad7 100644 --- a/Moonlight.Client/Services/IdentityService.cs +++ b/Moonlight.Client/Services/IdentityService.cs @@ -1,10 +1,7 @@ using MoonCore.Attributes; using MoonCore.Blazor.Services; -using MoonCore.Blazor.Tailwind.Services; using MoonCore.Exceptions; using MoonCore.Helpers; -using MoonCore.Models; -using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Responses.Auth; namespace Moonlight.Client.Services; diff --git a/Moonlight.Client/UI/Layouts/MainLayout.razor b/Moonlight.Client/UI/Layouts/MainLayout.razor index 6adfe13f..2a7e4bf8 100644 --- a/Moonlight.Client/UI/Layouts/MainLayout.razor +++ b/Moonlight.Client/UI/Layouts/MainLayout.razor @@ -1,7 +1,4 @@ @using MoonCore.Exceptions -@using MoonCore.Extensions -@using MoonCore.Helpers -@using MoonCore.PluginFramework.Services @using Moonlight.Client.Interfaces @using Moonlight.Client.Services @using Moonlight.Client.UI.Partials diff --git a/Moonlight.Client/UI/Partials/AppSidebar.razor b/Moonlight.Client/UI/Partials/AppSidebar.razor index 08ab0197..b73d69ba 100644 --- a/Moonlight.Client/UI/Partials/AppSidebar.razor +++ b/Moonlight.Client/UI/Partials/AppSidebar.razor @@ -1,5 +1,4 @@ -@using MoonCore.PluginFramework.Services -@using Moonlight.Client.Interfaces +@using Moonlight.Client.Interfaces @using Moonlight.Client.Models @using Moonlight.Client.Services @using Moonlight.Client.UI.Layouts diff --git a/Moonlight.Client/UI/Views/Admin/Api/Index.razor b/Moonlight.Client/UI/Views/Admin/Api/Index.razor index cd7e14aa..99433315 100644 --- a/Moonlight.Client/UI/Views/Admin/Api/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Api/Index.razor @@ -1,6 +1,6 @@ @page "/admin/api" -@using MoonCore.Blazor.Tailwind.Attributes +@using MoonCore.Attributes @using MoonCore.Helpers @using MoonCore.Models @using Moonlight.Shared.Http.Requests.Admin.ApiKeys diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Index.razor b/Moonlight.Client/UI/Views/Admin/Sys/Index.razor index 92dfaddb..66774b2a 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Index.razor @@ -1,6 +1,6 @@ @page "/admin/system" -@using MoonCore.Blazor.Tailwind.Attributes +@using MoonCore.Attributes @using Moonlight.Client.UI.Components @attribute [RequirePermission("admin.system.read")] diff --git a/Moonlight.Client/UI/Views/Admin/Users/Index.razor b/Moonlight.Client/UI/Views/Admin/Users/Index.razor index 6652fce9..55374a8e 100644 --- a/Moonlight.Client/UI/Views/Admin/Users/Index.razor +++ b/Moonlight.Client/UI/Views/Admin/Users/Index.razor @@ -1,6 +1,6 @@ @page "/admin/users" -@using MoonCore.Blazor.Tailwind.Attributes +@using MoonCore.Attributes @using MoonCore.Blazor.Tailwind.Forms.Components @using MoonCore.Helpers @using MoonCore.Models