Removed old manual access token checking and switched to asp.net jwt handling. Removed old console subscriber handling and switched to full signal r solution + asp.net core auth

This commit is contained in:
2025-04-13 00:09:06 +02:00
parent ec0c336825
commit 36cbc83c63
15 changed files with 181 additions and 380 deletions

View File

@@ -98,7 +98,7 @@ public class ServerFileSystemController : Controller
url += server.Node.UseSsl ? "https://" : "http://"; url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/"; url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
url += $"api/servers/upload?token={accessToken}"; url += $"api/servers/upload?access_token={accessToken}";
return new ServerFilesUploadResponse() return new ServerFilesUploadResponse()
{ {
@@ -126,7 +126,7 @@ public class ServerFileSystemController : Controller
url += server.Node.UseSsl ? "https://" : "http://"; url += server.Node.UseSsl ? "https://" : "http://";
url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/"; url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/";
url += $"api/servers/download?token={accessToken}"; url += $"api/servers/download?access_token={accessToken}";
return new ServerFilesDownloadResponse() return new ServerFilesDownloadResponse()
{ {

View File

@@ -132,7 +132,7 @@ public class ServersController : Controller
{ {
parameters.Add("type", "websocket"); parameters.Add("type", "websocket");
parameters.Add("serverId", server.Id); parameters.Add("serverId", server.Id);
}, TimeSpan.FromMinutes(10)); }, TimeSpan.FromSeconds(30));
var url = ""; var url = "";

View File

@@ -0,0 +1,28 @@
namespace MoonlightServers.ApiServer.Services;
public class NodeBootService : IHostedLifecycleService
{
public async Task StartedAsync(CancellationToken cancellationToken)
{
// TODO: Add node boot calls here
}
#region Unused
public Task StartAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
#endregion
}

View File

@@ -32,7 +32,8 @@ public class NodeService
node.Token node.Token
)), )),
SecurityAlgorithms.HmacSha256 SecurityAlgorithms.HmacSha256
) ),
Audience = node.TokenId
}; };
var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor); var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDescriptor);

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions; using MoonlightServers.Daemon.Extensions;
using Stateless; using Stateless;
@@ -98,21 +99,28 @@ public partial class Server
Logger.LogInformation("State: {state}", transition.Destination); Logger.LogInformation("State: {state}", transition.Destination);
}); });
// Proxy the events so outside subscribes can react to it // Proxy the events so outside subscribes can react to it and notify websockets
StateMachine.OnTransitionCompletedAsync(async transition => StateMachine.OnTransitionCompletedAsync(async transition =>
{ {
// Notify all clients interested in the server
await WebSocketHub.Clients
.Group(Id.ToString()) //TODO: Consider saving the string value in memory
.SendAsync("StateChanged", transition.Destination.ToString());
// Notify all external listeners
if (OnStateChanged != null) if (OnStateChanged != null)
{
await OnStateChanged(transition.Destination); await OnStateChanged(transition.Destination);
}
}); });
Console.OnOutput += (async message => Console.OnOutput += (async message =>
{ {
// Notify all clients interested in the server
await WebSocketHub.Clients
.Group(Id.ToString()) //TODO: Consider saving the string value in memory
.SendAsync("ConsoleOutput", message);
if (OnConsoleOutput != null) if (OnConsoleOutput != null)
{
await OnConsoleOutput(message); await OnConsoleOutput(message);
}
}); });
return Task.CompletedTask; return Task.CompletedTask;

View File

@@ -1,5 +1,7 @@
using Docker.DotNet.Models; using Docker.DotNet.Models;
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models; using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.Models.Cache; using MoonlightServers.Daemon.Models.Cache;
using Stateless; using Stateless;
@@ -11,24 +13,26 @@ public partial class Server
// Exposed configuration/state values // Exposed configuration/state values
public int Id => Configuration.Id; public int Id => Configuration.Id;
public ServerState State => StateMachine.State; public ServerState State => StateMachine.State;
// Exposed container names and ids // Exposed container names and ids
public string RuntimeContainerName { get; private set; } public string RuntimeContainerName { get; private set; }
public string? RuntimeContainerId { get; private set; } public string? RuntimeContainerId { get; private set; }
public string InstallationContainerName { get; private set; } public string InstallationContainerName { get; private set; }
public string? InstallationContainerId { get; private set; } public string? InstallationContainerId { get; private set; }
// Events // Events
public event Func<ServerState, Task> OnStateChanged; public event Func<ServerState, Task>? OnStateChanged;
public event Func<string, Task> OnConsoleOutput; public event Func<string, Task>? OnConsoleOutput;
// Private stuff // Private stuff
private readonly ILogger Logger; private readonly ILogger Logger;
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ServerConsole Console; private readonly ServerConsole Console;
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
private StateMachine<ServerState, ServerTrigger> StateMachine; private StateMachine<ServerState, ServerTrigger> StateMachine;
private ServerConfiguration Configuration; private ServerConfiguration Configuration;
private CancellationTokenSource Cancellation; private CancellationTokenSource Cancellation;
@@ -36,12 +40,14 @@ public partial class Server
public Server( public Server(
ILogger logger, ILogger logger,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ServerConfiguration configuration ServerConfiguration configuration,
IHubContext<ServerWebSocketHub> webSocketHub
) )
{ {
Logger = logger; Logger = logger;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
Configuration = configuration; Configuration = configuration;
WebSocketHub = webSocketHub;
Console = new(); Console = new();
Cancellation = new(); Cancellation = new();

View File

@@ -1,51 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Attributes;
using MoonCore.Extended.Helpers;
using MoonlightServers.Daemon.Configuration;
namespace MoonlightServers.Daemon.Helpers;
[Singleton]
public class AccessTokenHelper
{
private readonly AppConfiguration Configuration;
public AccessTokenHelper(AppConfiguration configuration)
{
Configuration = configuration;
}
// TODO: Improve
public bool Process(string accessToken, out Claim[] claims)
{
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;
}
}
}

View File

@@ -1,146 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Abstractions;
using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Helpers;
public class ServerWebSocketConnection
{
private readonly ServerService ServerService;
private readonly ILogger<ServerWebSocketConnection> Logger;
private readonly AccessTokenHelper AccessTokenHelper;
private readonly IHubContext<ServerWebSocketHub> HubContext;
private int ServerId = -1;
private Server Server;
private bool IsInitialized = false;
private string ConnectionId;
public ServerWebSocketConnection(
ServerService serverService,
ILogger<ServerWebSocketConnection> logger,
AccessTokenHelper accessTokenHelper,
IHubContext<ServerWebSocketHub> hubContext
)
{
ServerService = serverService;
Logger = logger;
AccessTokenHelper = accessTokenHelper;
HubContext = hubContext;
}
public Task Initialize(HubCallerContext context) => Task.CompletedTask;
public async Task Authenticate(HubCallerContext context, string accessToken)
{
// Validate access token
if (!AccessTokenHelper.Process(accessToken, out var accessData))
{
Logger.LogDebug("Received invalid or expired access token");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid or expired access token"
);
return;
}
// Validate access token data
if (accessData.All(x => x.Type != "type") || accessData.All(x => x.Type != "serverId"))
{
Logger.LogDebug("Received invalid access token: Required parameters are missing");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid access token: Required parameters are missing"
);
return;
}
// Validate access token type
var type = accessData.First(x => x.Type == "type").Value;
if (type != "websocket")
{
Logger.LogDebug("Received invalid access token: Invalid type '{type}'", type);
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
$"Received invalid access token: Invalid type '{type}'"
);
return;
}
var serverId = int.Parse(accessData.First(x => x.Type == "serverId").Value);
// Check that the access token isn't for another server
if (ServerId != -1 && ServerId == serverId)
{
Logger.LogDebug("Received invalid access token: Server id not valid for this session. Current server id: {serverId}", ServerId);
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
$"Received invalid access token: Server id not valid for this session. Current server id: {ServerId}"
);
return;
}
var server = ServerService.GetServer(serverId);
// Check i the server actually exists
if (server == null)
{
Logger.LogDebug("Received invalid access token: No server found with the requested id");
await HubContext.Clients.Client(context.ConnectionId).SendAsync(
"Error",
"Received invalid access token: No server found with the requested id"
);
return;
}
// Set values
Server = server;
ServerId = serverId;
ConnectionId = context.ConnectionId;
if(IsInitialized)
return;
IsInitialized = true;
// Setup event handlers
Server.OnConsoleOutput += HandleConsoleOutput;
Server.OnStateChanged += HandleStateChange;
Logger.LogTrace("Authenticated and initialized server console connection '{id}'", context.ConnectionId);
}
public Task Destroy(HubCallerContext context)
{
Logger.LogTrace("Destroyed server console connection '{id}'", context.ConnectionId);
Server.OnConsoleOutput -= HandleConsoleOutput;
Server.OnStateChanged -= HandleStateChange;
return Task.CompletedTask;
}
#region Event Handlers
private async Task HandleStateChange(ServerState state)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("StateChanged", state.ToString());
private async Task HandleConsoleOutput(string line)
=> await HubContext.Clients.Client(ConnectionId).SendAsync("ConsoleOutput", line);
#endregion
}

View File

@@ -2,66 +2,34 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonlightServers.Daemon.Configuration; using MoonlightServers.Daemon.Configuration;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Services; using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers; namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[AllowAnonymous]
[ApiController] [ApiController]
[Route("api/servers/download")] [Route("api/servers/download")]
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverDownload")]
public class DownloadController : Controller public class DownloadController : Controller
{ {
private readonly AccessTokenHelper AccessTokenHelper;
private readonly AppConfiguration Configuration;
private readonly ServerService ServerService; private readonly ServerService ServerService;
public DownloadController( public DownloadController(ServerService serverService)
AccessTokenHelper accessTokenHelper,
ServerService serverService,
AppConfiguration configuration
)
{ {
AccessTokenHelper = accessTokenHelper;
ServerService = serverService; ServerService = serverService;
Configuration = configuration;
} }
[HttpGet] [HttpGet]
public async Task Download([FromQuery] string token) public async Task Download()
{ {
#region Token validation var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
var path = User.Claims.First(x => x.Type == "path").Value;
if (!AccessTokenHelper.Process(token, out var claims))
throw new HttpApiException("Invalid access token provided", 401);
var typeClaim = claims.FirstOrDefault(x => x.Type == "type");
if (typeClaim == null || typeClaim.Value != "download")
throw new HttpApiException("Invalid access token provided: Missing or invalid type", 401);
var serverIdClaim = claims.FirstOrDefault(x => x.Type == "serverId");
if (serverIdClaim == null || !int.TryParse(serverIdClaim.Value, out var serverId))
throw new HttpApiException("Invalid access token provided: Missing or invalid server id", 401);
var pathClaim = claims.FirstOrDefault(x => x.Type == "path");
if(pathClaim == null || string.IsNullOrEmpty(pathClaim.Value))
throw new HttpApiException("Invalid access token provided: Missing or invalid path", 401);
#endregion
var server = ServerService.GetServer(serverId); var server = ServerService.GetServer(serverId);
if (server == null) if (server == null)
throw new HttpApiException("No server with this id found", 404); throw new HttpApiException("No server with this id found", 404);
var path = pathClaim.Value; await server.FileSystem.Read(path,
async dataStream => { await Results.File(dataStream).ExecuteAsync(HttpContext); });
await server.FileSystem.Read(path, async dataStream =>
{
await Results.File(dataStream).ExecuteAsync(HttpContext);
});
} }
} }

View File

@@ -9,30 +9,26 @@ using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Controllers.Servers; namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[ApiController] [ApiController]
[AllowAnonymous]
[Route("api/servers/upload")] [Route("api/servers/upload")]
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverUpload")]
public class UploadController : Controller public class UploadController : Controller
{ {
private readonly AccessTokenHelper AccessTokenHelper;
private readonly AppConfiguration Configuration; private readonly AppConfiguration Configuration;
private readonly ServerService ServerService; private readonly ServerService ServerService;
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config
public UploadController( public UploadController(
AccessTokenHelper accessTokenHelper,
ServerService serverService, ServerService serverService,
AppConfiguration configuration AppConfiguration configuration
) )
{ {
AccessTokenHelper = accessTokenHelper;
ServerService = serverService; ServerService = serverService;
Configuration = configuration; Configuration = configuration;
} }
[HttpPost] [HttpPost]
public async Task Upload( public async Task Upload(
[FromQuery] string token,
[FromQuery] long totalSize, // TODO: Add limit in config [FromQuery] long totalSize, // TODO: Add limit in config
[FromQuery] int chunkId, [FromQuery] int chunkId,
[FromQuery] string path [FromQuery] string path
@@ -50,22 +46,7 @@ public class UploadController : Controller
#endregion #endregion
#region Token validation var serverId = int.Parse(User.Claims.First(x => x.Type == "serverId").Value);
if (!AccessTokenHelper.Process(token, out var claims))
throw new HttpApiException("Invalid access token provided", 401);
var typeClaim = claims.FirstOrDefault(x => x.Type == "type");
if (typeClaim == null || typeClaim.Value != "upload")
throw new HttpApiException("Invalid access token provided: Missing or invalid type", 401);
var serverIdClaim = claims.FirstOrDefault(x => x.Type == "serverId");
if (serverIdClaim == null || !int.TryParse(serverIdClaim.Value, out var serverId))
throw new HttpApiException("Invalid access token provided: Missing or invalid server id", 401);
#endregion
#region Chunk calculation and validation #region Chunk calculation and validation

View File

@@ -1,43 +1,28 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using MoonlightServers.Daemon.Services;
namespace MoonlightServers.Daemon.Http.Hubs; namespace MoonlightServers.Daemon.Http.Hubs;
[Authorize(AuthenticationSchemes = "accessToken", Policy = "serverWebsocket")]
public class ServerWebSocketHub : Hub public class ServerWebSocketHub : Hub
{ {
private readonly ILogger<ServerWebSocketHub> Logger; private readonly ILogger<ServerWebSocketHub> Logger;
private readonly ServerWebSocketService WebSocketService;
public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger, ServerWebSocketService webSocketService) public ServerWebSocketHub(ILogger<ServerWebSocketHub> logger)
{ {
Logger = logger; Logger = logger;
WebSocketService = webSocketService;
} }
#region Connection Handlers
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
=> await WebSocketService.InitializeClient(Context);
public override async Task OnDisconnectedAsync(Exception? exception)
=> await WebSocketService.DestroyClient(Context);
#endregion
#region Methods
[HubMethodName("Authenticate")]
public async Task Authenticate(string accessToken)
{ {
try // The policies validated already the type and the token so we can assume we are authenticated
{ // and just start adding ourselves into the desired group
await WebSocketService.AuthenticateClient(Context, accessToken);
} var serverId = Context.User!.Claims.First(x => x.Type == "serverId").Value;
catch (Exception e)
{
Logger.LogError("An unhandled error occured in the Authenticate method: {e}", e);
}
}
#endregion await Groups.AddToGroupAsync(
Context.ConnectionId,
serverId
);
}
} }

View File

@@ -1,11 +1,13 @@
using Docker.DotNet; using Docker.DotNet;
using Docker.DotNet.Models; using Docker.DotNet.Models;
using Microsoft.AspNetCore.SignalR;
using MoonCore.Attributes; using MoonCore.Attributes;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Models; using MoonCore.Models;
using MoonlightServers.Daemon.Abstractions; using MoonlightServers.Daemon.Abstractions;
using MoonlightServers.Daemon.Enums; using MoonlightServers.Daemon.Enums;
using MoonlightServers.Daemon.Extensions; using MoonlightServers.Daemon.Extensions;
using MoonlightServers.Daemon.Http.Hubs;
using MoonlightServers.Daemon.Models.Cache; using MoonlightServers.Daemon.Models.Cache;
using MoonlightServers.DaemonShared.PanelSide.Http.Responses; using MoonlightServers.DaemonShared.PanelSide.Http.Responses;
@@ -19,6 +21,7 @@ public class ServerService : IHostedLifecycleService
private readonly RemoteService RemoteService; private readonly RemoteService RemoteService;
private readonly IServiceProvider ServiceProvider; private readonly IServiceProvider ServiceProvider;
private readonly ILoggerFactory LoggerFactory; private readonly ILoggerFactory LoggerFactory;
private readonly IHubContext<ServerWebSocketHub> WebSocketHub;
private CancellationTokenSource Cancellation = new(); private CancellationTokenSource Cancellation = new();
private bool IsInitialized = false; private bool IsInitialized = false;
@@ -26,13 +29,15 @@ public class ServerService : IHostedLifecycleService
RemoteService remoteService, RemoteService remoteService,
ILogger<ServerService> logger, ILogger<ServerService> logger,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory,
IHubContext<ServerWebSocketHub> webSocketHub
) )
{ {
RemoteService = remoteService; RemoteService = remoteService;
Logger = logger; Logger = logger;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
WebSocketHub = webSocketHub;
} }
public async Task Initialize() //TODO: Add initialize call from panel public async Task Initialize() //TODO: Add initialize call from panel
@@ -190,7 +195,8 @@ public class ServerService : IHostedLifecycleService
var server = new Server( var server = new Server(
LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"), LoggerFactory.CreateLogger($"Server {serverConfiguration.Id}"),
ServiceProvider, ServiceProvider,
serverConfiguration serverConfiguration,
WebSocketHub
); );
await server.Initialize(existingContainers); await server.Initialize(existingContainers);

View File

@@ -1,68 +0,0 @@
using Microsoft.AspNetCore.SignalR;
using MoonCore.Attributes;
using MoonlightServers.Daemon.Helpers;
using MoonlightServers.Daemon.Http.Hubs;
namespace MoonlightServers.Daemon.Services;
[Singleton]
public class ServerWebSocketService
{
private readonly ILogger<ServerWebSocketService> Logger;
private readonly IServiceProvider ServiceProvider;
private readonly Dictionary<string, ServerWebSocketConnection> Connections = new();
public ServerWebSocketService(
ILogger<ServerWebSocketService> logger,
IServiceProvider serviceProvider
)
{
Logger = logger;
ServiceProvider = serviceProvider;
}
public async Task InitializeClient(HubCallerContext context)
{
var connection = new ServerWebSocketConnection(
ServiceProvider.GetRequiredService<ServerService>(),
ServiceProvider.GetRequiredService<ILogger<ServerWebSocketConnection>>(),
ServiceProvider.GetRequiredService<AccessTokenHelper>(),
ServiceProvider.GetRequiredService<IHubContext<ServerWebSocketHub>>()
);
lock (Connections)
Connections[context.ConnectionId] = connection;
await connection.Initialize(context);
}
public async Task AuthenticateClient(HubCallerContext context, string accessToken)
{
ServerWebSocketConnection? connection;
lock (Connections)
connection = Connections.GetValueOrDefault(context.ConnectionId);
if(connection == null)
return;
await connection.Authenticate(context, accessToken);
}
public async Task DestroyClient(HubCallerContext context)
{
ServerWebSocketConnection? connection;
lock (Connections)
connection = Connections.GetValueOrDefault(context.ConnectionId);
if(connection == null)
return;
await connection.Destroy(context);
lock (Connections)
Connections.Remove(context.ConnectionId);
}
}

View File

@@ -1,5 +1,9 @@
using System.Text;
using System.Text.Json; using System.Text.Json;
using Docker.DotNet; using Docker.DotNet;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.IdentityModel.Tokens;
using MoonCore.EnvConfiguration; using MoonCore.EnvConfiguration;
using MoonCore.Extended.Extensions; using MoonCore.Extended.Extensions;
using MoonCore.Extensions; using MoonCore.Extensions;
@@ -50,8 +54,8 @@ public class Startup
await BuildWebApplication(); await BuildWebApplication();
await UseBase(); await UseBase();
await UseAuth();
await UseCors(); await UseCors();
await UseAuth();
await UseBaseMiddleware(); await UseBaseMiddleware();
await MapBase(); await MapBase();
@@ -273,7 +277,11 @@ public class Startup
private Task MapHubs() private Task MapHubs()
{ {
WebApplication.MapHub<ServerWebSocketHub>("api/servers/ws"); WebApplication.MapHub<ServerWebSocketHub>("api/servers/ws", options =>
{
options.AllowStatefulReconnects = false;
options.CloseOnAuthenticationExpiration = true;
});
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -286,8 +294,11 @@ public class Startup
{ {
//TODO: IMPORTANT: CHANGE !!! //TODO: IMPORTANT: CHANGE !!!
WebApplicationBuilder.Services.AddCors(x => WebApplicationBuilder.Services.AddCors(x =>
x.AddDefaultPolicy(builder => x.AddDefaultPolicy(builder => builder
builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().Build() .SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
) )
); );
@@ -308,12 +319,78 @@ public class Startup
{ {
WebApplicationBuilder.Services WebApplicationBuilder.Services
.AddAuthentication("token") .AddAuthentication("token")
.AddScheme<TokenAuthOptions, TokenAuthScheme>("token", options => .AddScheme<TokenAuthOptions, TokenAuthScheme>("token",
options => { options.Token = Configuration.Security.Token; })
.AddJwtBearer("accessToken", options =>
{ {
options.Token = Configuration.Security.Token; options.TokenValidationParameters = new TokenValidationParameters()
{
ClockSkew = TimeSpan.Zero,
ValidateAudience = true,
ValidateIssuer = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidateActor = false,
IssuerSigningKeys =
[
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.Security.Token)
)
],
AudienceValidator = (audiences, _, _)
=> audiences.Contains(Configuration.Security.TokenId)
};
// Some requests like the signal r websockets or the upload and download endpoints require the user to provide
// the access token via the query instead of the header field due to client limitations. To reuse the authentication
// on all these requests as we do for all other endpoints which require an access token, we are defining
// the custom behaviour to load the access token from the query for these specific endpoints below
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
if (
!context.HttpContext.Request.Path.StartsWithSegments("/api/servers/ws") &&
!context.HttpContext.Request.Path.StartsWithSegments("/api/servers/upload") &&
!context.HttpContext.Request.Path.StartsWithSegments("/api/servers/download")
)
{
return Task.CompletedTask;
}
var accessToken = context.Request.Query["access_token"];
if (string.IsNullOrEmpty(accessToken))
return Task.CompletedTask;
context.Token = accessToken;
return Task.CompletedTask;
}
};
}); });
WebApplicationBuilder.Services.AddAuthorization(); WebApplicationBuilder.Services.AddAuthorization(options =>
{
// We are defining the access token policies here. Because the same jwt secret is used by the panel
// to generate jwt access tokens for all sorts of daemon related stuff we need to separate
// the type of access token using the type parameter provided in the claims.
options.AddPolicy("serverWebsocket", builder =>
{
builder.RequireClaim("type", "websocket");
});
options.AddPolicy("serverUpload", builder =>
{
builder.RequireClaim("type", "upload");
});
options.AddPolicy("serverDownload", builder =>
{
builder.RequireClaim("type", "download");
});
});
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,6 +1,7 @@
@page "/servers/{ServerId:int}" @page "/servers/{ServerId:int}"
@page "/servers/{ServerId:int}/{TabPath:alpha}" @page "/servers/{ServerId:int}/{TabPath:alpha}"
@using Microsoft.AspNetCore.Http.Connections
@using Microsoft.AspNetCore.SignalR.Client @using Microsoft.AspNetCore.SignalR.Client
@using MoonlightServers.Shared.Http.Responses.Users.Servers @using MoonlightServers.Shared.Http.Responses.Users.Servers
@using MoonCore.Blazor.Tailwind.Components @using MoonCore.Blazor.Tailwind.Components
@@ -224,7 +225,15 @@
// Build signal r // Build signal r
HubConnection = new HubConnectionBuilder() HubConnection = new HubConnectionBuilder()
.WithUrl(websocketDetails.Target) .WithUrl(websocketDetails.Target, options =>
{
options.AccessTokenProvider = async () =>
{
var details = await ServerService.GetWebSocket(ServerId);
return details.AccessToken;
};
})
.WithAutomaticReconnect()
.Build(); .Build();
// Define handlers // Define handlers
@@ -246,9 +255,6 @@
// Connect // Connect
await HubConnection.StartAsync(); await HubConnection.StartAsync();
// Authenticate
await HubConnection.SendAsync("Authenticate", websocketDetails.AccessToken);
} }
catch (HttpApiException e) catch (HttpApiException e)
{ {