Improved jwt handling for node access tokens. Switched to di plugin system
This commit is contained in:
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Services;
|
using MoonlightServers.ApiServer.Services;
|
||||||
|
|
||||||
@@ -117,7 +118,9 @@ public class ServerPowerController : Controller
|
|||||||
if (server.OwnerId == userId) // The current user is the owner
|
if (server.OwnerId == userId) // The current user is the owner
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
if (User.HasPermission("admin.servers.get")) // The current user is an admin
|
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using MoonCore.Extended.PermFilter;
|
|||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
@@ -198,7 +199,9 @@ public class ServersController : Controller
|
|||||||
if (server.OwnerId == userId) // The current user is the owner
|
if (server.OwnerId == userId) // The current user is the owner
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
if (User.HasPermission("admin.servers.get")) // The current user is an admin
|
var permissions = User.Claims.First(x => x.Type == "permissions").Value.Split(";", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (PermissionHelper.HasPermission(permissions, "admin.servers.get")) // The current user is an admin
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
@@ -20,20 +24,43 @@ public class NodeService
|
|||||||
url += "http://";
|
url += "http://";
|
||||||
|
|
||||||
url += $"{node.Fqdn}:{node.HttpPort}/";
|
url += $"{node.Fqdn}:{node.HttpPort}/";
|
||||||
|
|
||||||
var httpClient = new HttpClient()
|
var httpClient = new HttpClient()
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri(url)
|
BaseAddress = new Uri(url)
|
||||||
};
|
};
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}");
|
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}");
|
||||||
|
|
||||||
return new HttpApiClient(httpClient);
|
return new HttpApiClient(httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
|
public string CreateAccessToken(Node node, Action<Dictionary<string, object>> parameters, TimeSpan duration)
|
||||||
=> JwtHelper.Encode(node.Token, parameters, duration);
|
{
|
||||||
|
var claims = new Dictionary<string, object>();
|
||||||
|
parameters.Invoke(claims);
|
||||||
|
|
||||||
|
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
var securityTokenDescriptor = new SecurityTokenDescriptor()
|
||||||
|
{
|
||||||
|
Expires = DateTime.UtcNow.Add(duration),
|
||||||
|
NotBefore = DateTime.UtcNow.AddSeconds(-1),
|
||||||
|
Claims = claims,
|
||||||
|
IssuedAt = DateTime.UtcNow,
|
||||||
|
SigningCredentials = new SigningCredentials(
|
||||||
|
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
node.Token
|
||||||
|
)),
|
||||||
|
SecurityAlgorithms.HmacSha256
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);
|
||||||
|
|
||||||
|
return jwtSecurityTokenHandler.WriteToken(securityToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SystemStatusResponse> GetSystemStatus(Node node)
|
public async Task<SystemStatusResponse> GetSystemStatus(Node node)
|
||||||
{
|
{
|
||||||
using var apiClient = await CreateApiClient(node);
|
using var apiClient = await CreateApiClient(node);
|
||||||
@@ -47,13 +74,13 @@ public class NodeService
|
|||||||
using var apiClient = await CreateApiClient(node);
|
using var apiClient = await CreateApiClient(node);
|
||||||
return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application");
|
return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StatisticsHostResponse> GetHostStatistics(Node node)
|
public async Task<StatisticsHostResponse> GetHostStatistics(Node node)
|
||||||
{
|
{
|
||||||
using var apiClient = await CreateApiClient(node);
|
using var apiClient = await CreateApiClient(node);
|
||||||
return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host");
|
return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
|
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
|
||||||
{
|
{
|
||||||
using var apiClient = await CreateApiClient(node);
|
using var apiClient = await CreateApiClient(node);
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
using Moonlight.ApiServer.Helpers;
|
|
||||||
using Moonlight.ApiServer.Interfaces.Startup;
|
|
||||||
using MoonlightServers.ApiServer.Database;
|
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Startup;
|
|
||||||
|
|
||||||
public class DatabaseStartup : IDatabaseStartup
|
|
||||||
{
|
|
||||||
public Task ConfigureDatabase(DatabaseContextCollection collection)
|
|
||||||
{
|
|
||||||
collection.Add<ServersDataContext>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
|
using Moonlight.ApiServer.Helpers;
|
||||||
using Moonlight.ApiServer.Interfaces.Startup;
|
using Moonlight.ApiServer.Interfaces.Startup;
|
||||||
using Moonlight.ApiServer.Services;
|
using Moonlight.ApiServer.Services;
|
||||||
|
using MoonlightServers.ApiServer.Database;
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Startup;
|
namespace MoonlightServers.ApiServer.Startup;
|
||||||
|
|
||||||
public class PluginStartup : IAppStartup
|
public class PluginStartup : IPluginStartup
|
||||||
{
|
{
|
||||||
private readonly BundleService BundleService;
|
private readonly BundleService BundleService;
|
||||||
|
|
||||||
@@ -13,19 +15,27 @@ public class PluginStartup : IAppStartup
|
|||||||
BundleService = bundleService;
|
BundleService = bundleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task BuildApp(IHostApplicationBuilder builder)
|
public Task BuildApplication(IHostApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
// Scan the current plugin assembly for di services
|
// Scan the current plugin assembly for di services
|
||||||
builder.Services.AutoAddServices<PluginStartup>();
|
builder.Services.AutoAddServices<PluginStartup>();
|
||||||
|
|
||||||
BundleService.BundleCss("css/MoonlightServers.min.css");
|
BundleService.BundleCss("css/MoonlightServers.min.css");
|
||||||
BundleService.BundleCss("css/XtermBlazor.min.css");
|
BundleService.BundleCss("css/XtermBlazor.min.css");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ConfigureApp(IApplicationBuilder app)
|
public Task ConfigureApplication(IApplicationBuilder app)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task ConfigureDatabase(DatabaseContextCollection collection)
|
||||||
{
|
{
|
||||||
|
collection.Add<ServersDataContext>();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
|
||||||
|
=> Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ public partial class Server
|
|||||||
public async Task InternalError()
|
public async Task InternalError()
|
||||||
{
|
{
|
||||||
await LogToConsole("An unhandled error occured performing action");
|
await LogToConsole("An unhandled error occured performing action");
|
||||||
|
// TODO:
|
||||||
Logger.LogInformation("Reporting or smth");
|
Logger.LogInformation("Reporting or smth");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
@@ -15,8 +19,33 @@ public class AccessTokenHelper
|
|||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Process(string accessToken, out Dictionary<string, JsonElement> data)
|
// TODO: Improve
|
||||||
|
public bool Process(string accessToken, out Claim[] claims)
|
||||||
{
|
{
|
||||||
return JwtHelper.TryVerifyAndDecodePayload(Configuration.Security.Token, accessToken, out data);
|
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = jwtSecurityTokenHandler.ValidateToken(accessToken, new()
|
||||||
|
{
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateActor = false,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(
|
||||||
|
Encoding.UTF8.GetBytes(Configuration.Security.Token)
|
||||||
|
)
|
||||||
|
}, out var _);
|
||||||
|
|
||||||
|
claims = data.Claims.ToArray();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
claims = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ public class ServerWebSocketConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate access token data
|
// Validate access token data
|
||||||
if (!accessData.ContainsKey("type") || !accessData.ContainsKey("serverId"))
|
if (accessData.All(x => x.Type != "type") || accessData.All(x => x.Type != "serverId"))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Received invalid access token: Required parameters are missing");
|
Logger.LogDebug("Received invalid access token: Required parameters are missing");
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ public class ServerWebSocketConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate access token type
|
// Validate access token type
|
||||||
var type = accessData["type"].GetString()!;
|
var type = accessData.First(x => x.Type == "type").Value;
|
||||||
|
|
||||||
if (type != "websocket")
|
if (type != "websocket")
|
||||||
{
|
{
|
||||||
@@ -77,7 +77,7 @@ public class ServerWebSocketConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverId = accessData["serverId"].GetInt32();
|
var serverId = int.Parse(accessData.First(x => x.Type == "serverId").Value);
|
||||||
|
|
||||||
// Check that the access token isn't for another server
|
// Check that the access token isn't for another server
|
||||||
if (ServerId != -1 && ServerId == serverId)
|
if (ServerId != -1 && ServerId == serverId)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="MoonCore" Version="1.8.2" />
|
<PackageReference Include="MoonCore" Version="1.8.3" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
|
<PackageReference Include="MoonCore.Extended" Version="1.2.8" />
|
||||||
<PackageReference Include="MoonCore.Unix" Version="1.0.0" />
|
<PackageReference Include="MoonCore.Unix" Version="1.0.2" />
|
||||||
<PackageReference Include="Stateless" Version="5.17.0" />
|
<PackageReference Include="Stateless" Version="5.17.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.PluginFramework.Extensions;
|
|
||||||
using Moonlight.Client.Interfaces;
|
using Moonlight.Client.Interfaces;
|
||||||
|
using MoonlightServers.Frontend.Implementations;
|
||||||
using MoonlightServers.Frontend.Interfaces;
|
using MoonlightServers.Frontend.Interfaces;
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Startup;
|
namespace MoonlightServers.Frontend.Startup;
|
||||||
|
|
||||||
public class PluginStartup : IAppStartup
|
public class PluginStartup : IPluginStartup
|
||||||
{
|
{
|
||||||
public Task BuildApp(WebAssemblyHostBuilder builder)
|
public Task BuildApplication(WebAssemblyHostBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.Services.AddSingleton<ISidebarItemProvider, SidebarImplementation>();
|
||||||
|
builder.Services.AddSingleton<IServerTabProvider, DefaultServerTabProvider>();
|
||||||
|
|
||||||
builder.Services.AutoAddServices<PluginStartup>();
|
builder.Services.AutoAddServices<PluginStartup>();
|
||||||
|
|
||||||
builder.Services.AddInterfaces(configuration =>
|
|
||||||
{
|
|
||||||
configuration.AddAssembly(GetType().Assembly);
|
|
||||||
|
|
||||||
configuration.AddInterface<IServerTabProvider>();
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ConfigureApp(WebAssemblyHost app)
|
public Task ConfigureApplication(WebAssemblyHost app)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
@using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs
|
@using MoonlightServers.Frontend.UI.Components.Servers.ServerTabs
|
||||||
|
|
||||||
@inject HttpApiClient ApiClient
|
@inject HttpApiClient ApiClient
|
||||||
@inject IServerTabProvider[] TabProviders
|
@inject IEnumerable<IServerTabProvider> TabProviders
|
||||||
|
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user