diff --git a/MoonlightServers.ApiServer/Helpers/NodeAuthOptions.cs b/MoonlightServers.ApiServer/Helpers/NodeAuthOptions.cs new file mode 100644 index 0000000..696063c --- /dev/null +++ b/MoonlightServers.ApiServer/Helpers/NodeAuthOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace MoonlightServers.ApiServer.Helpers; + +public class NodeAuthOptions : AuthenticationSchemeOptions +{ + +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Helpers/NodeAuthScheme.cs b/MoonlightServers.ApiServer/Helpers/NodeAuthScheme.cs new file mode 100644 index 0000000..bdceace --- /dev/null +++ b/MoonlightServers.ApiServer/Helpers/NodeAuthScheme.cs @@ -0,0 +1,76 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using MoonCore.Extended.Abstractions; +using MoonlightServers.ApiServer.Database.Entities; + +namespace MoonlightServers.ApiServer.Helpers; + +public class NodeAuthScheme : AuthenticationHandler +{ + public NodeAuthScheme(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + public NodeAuthScheme(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base( + options, logger, encoder) + { + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + return AuthenticateResult.NoResult(); + + var authHeaderValue = Request.Headers["Authorization"].FirstOrDefault(); + + if (string.IsNullOrEmpty(authHeaderValue)) + return AuthenticateResult.NoResult(); + + if (!authHeaderValue.Contains("Bearer ")) + return AuthenticateResult.NoResult(); + + var tokenParts = authHeaderValue + .Replace("Bearer ", "") + .Trim() + .Split('.'); + + if (tokenParts.Length != 2) + return AuthenticateResult.NoResult(); + + var tokenId = tokenParts[0]; + var token = tokenParts[1]; + + if (tokenId.Length != 6) + return AuthenticateResult.NoResult(); + + var nodeRepo = Context.RequestServices.GetRequiredService>(); + + var node = await nodeRepo + .Get() + .FirstOrDefaultAsync(x => x.TokenId == tokenId); + + if (node == null) + return AuthenticateResult.NoResult(); + + if (node.Token != token) + return AuthenticateResult.NoResult(); + + return AuthenticateResult.Success( + new AuthenticationTicket( + new ClaimsPrincipal( + new ClaimsIdentity( + [ + new Claim("nodeId", node.Id.ToString()) + ], + "nodeAuthentication" + ) + ), + "nodeAuthentication" + ) + ); + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs index a9da3d4..884c59c 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/NodesController.cs @@ -48,8 +48,8 @@ public class NodesController : Controller { var node = Mapper.Map(request); - node.Token = Formatter.GenerateString(32); node.TokenId = Formatter.GenerateString(6); + node.Token = Formatter.GenerateString(32); var finalNode = await NodeRepository.Add(node); diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs index 68fafe1..1d393e2 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/Nodes/NodeTripController.cs @@ -5,7 +5,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Remote.Nodes; [ApiController] [Route("api/remote/server/node")] -[Authorize(AuthenticationSchemes = "serverNodeAuthentication")] +[Authorize(AuthenticationSchemes = "nodeAuthentication")] public class NodeTripController : Controller { [HttpGet("trip")] diff --git a/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs b/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs index 82ce01c..48ff11e 100644 --- a/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs +++ b/MoonlightServers.ApiServer/Http/Controllers/Remote/ServersController.cs @@ -11,7 +11,7 @@ namespace MoonlightServers.ApiServer.Http.Controllers.Remote; [ApiController] [Route("api/remote/servers")] -[Authorize(AuthenticationSchemes = "serverNodeAuthentication")] +[Authorize(AuthenticationSchemes = "nodeAuthentication")] public class ServersController : Controller { private readonly DatabaseRepository ServerRepository; @@ -21,7 +21,8 @@ public class ServersController : Controller public ServersController( DatabaseRepository serverRepository, DatabaseRepository nodeRepository, - ILogger logger) + ILogger logger + ) { ServerRepository = serverRepository; NodeRepository = nodeRepository; @@ -31,12 +32,12 @@ public class ServersController : Controller [HttpGet] public async Task> Get([FromQuery] int page, [FromQuery] int pageSize) { - // Load the node via the token id - var tokenId = User.Claims.First(x => x.Type == "iss").Value; + // Load the node via the id + var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value); var node = await NodeRepository .Get() - .FirstAsync(x => x.TokenId == tokenId); + .FirstAsync(x => x.Id == nodeId); var total = await ServerRepository .Get() @@ -79,12 +80,12 @@ public class ServersController : Controller [HttpGet("{id:int}")] public async Task Get([FromRoute] int id) { - // Load the node via the token id - var tokenId = User.Claims.First(x => x.Type == "iss").Value; + // Load the node via the id + var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value); var node = await NodeRepository .Get() - .FirstAsync(x => x.TokenId == tokenId); + .FirstAsync(x => x.Id == nodeId); // Load the server with the star data attached. We filter by the node to ensure the node can only access // servers linked to it @@ -111,12 +112,12 @@ public class ServersController : Controller [HttpGet("{id:int}/install")] public async Task GetInstall([FromRoute] int id) { - // Load the node via the token id - var tokenId = User.Claims.First(x => x.Type == "iss").Value; + // Load the node via the id + var nodeId = int.Parse(User.Claims.First(x => x.Type == "nodeId").Value); var node = await NodeRepository .Get() - .FirstAsync(x => x.TokenId == tokenId); + .FirstAsync(x => x.Id == nodeId); // Load the server with the star data attached. We filter by the node to ensure the node can only access // servers linked to it diff --git a/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs b/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs deleted file mode 100644 index 2e09e2e..0000000 --- a/MoonlightServers.ApiServer/Implementations/NodeJwtBearerOptions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Text; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using MoonCore.Extended.Abstractions; -using MoonlightServers.ApiServer.Database.Entities; - -namespace MoonlightServers.ApiServer.Implementations; - -public class NodeJwtBearerOptions : IConfigureNamedOptions -{ - private readonly IServiceProvider ServiceProvider; - - public NodeJwtBearerOptions(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - - public void Configure(JwtBearerOptions options) - { - } - - public void Configure(string? name, JwtBearerOptions options) - { - // Dont configure any other scheme - if (name != "serverNodeAuthentication") - return; - - options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, kid, _) => - { - if (string.IsNullOrEmpty(kid)) - return []; - - if (kid.Length != 6) - return []; - - using var scope = ServiceProvider.CreateScope(); - - var nodeRepo = scope.ServiceProvider.GetRequiredService>(); - - var node = nodeRepo - .Get() - .FirstOrDefault(x => x.TokenId == kid); - - if (node == null) - return []; - - return - [ - new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(node.Token) - ) - ]; - }; - } -} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj index 19b7b93..609b7b9 100644 --- a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj +++ b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj @@ -21,8 +21,8 @@ - + diff --git a/MoonlightServers.ApiServer/Services/NodeService.cs b/MoonlightServers.ApiServer/Services/NodeService.cs index ea9f3ec..1704071 100644 --- a/MoonlightServers.ApiServer/Services/NodeService.cs +++ b/MoonlightServers.ApiServer/Services/NodeService.cs @@ -69,29 +69,6 @@ public class NodeService #endregion #region Helpers - - private string GenerateJwt(Node node) - { - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - - var securityTokenDescriptor = new SecurityTokenDescriptor() - { - //Expires = DateTime.UtcNow.AddYears(1), - Expires = DateTime.UtcNow.AddMinutes(1), - NotBefore = DateTime.UtcNow.AddSeconds(-1), - 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 HttpApiClient CreateApiClient(Node node) { @@ -105,8 +82,7 @@ public class NodeService BaseAddress = new Uri(url) }; - var jwt = GenerateJwt(node); - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}"); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {node.Token}"); return new HttpApiClient(httpClient); } diff --git a/MoonlightServers.ApiServer/Startup/PluginStartup.cs b/MoonlightServers.ApiServer/Startup/PluginStartup.cs index 4a0bf54..c81e20f 100644 --- a/MoonlightServers.ApiServer/Startup/PluginStartup.cs +++ b/MoonlightServers.ApiServer/Startup/PluginStartup.cs @@ -1,9 +1,7 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; using MoonCore.Extensions; using Moonlight.ApiServer.Interfaces.Startup; using MoonlightServers.ApiServer.Database; -using MoonlightServers.ApiServer.Implementations; +using MoonlightServers.ApiServer.Helpers; namespace MoonlightServers.ApiServer.Startup; @@ -19,20 +17,7 @@ public class PluginStartup : IPluginStartup // Configure authentication for the remote endpoints builder.Services .AddAuthentication() - .AddJwtBearer("serverNodeAuthentication", options => - { - options.TokenValidationParameters = new() - { - ClockSkew = TimeSpan.Zero, - ValidateIssuer = false, - ValidateActor = false, - ValidateLifetime = true, - ValidateAudience = false, - ValidateIssuerSigningKey = true - }; - }); - - builder.Services.AddSingleton, NodeJwtBearerOptions>(); + .AddScheme("nodeAuthentication", null); return Task.CompletedTask; } diff --git a/MoonlightServers.Daemon/Helpers/TokenAuthOptions.cs b/MoonlightServers.Daemon/Helpers/TokenAuthOptions.cs new file mode 100644 index 0000000..d853d95 --- /dev/null +++ b/MoonlightServers.Daemon/Helpers/TokenAuthOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace MoonlightServers.Daemon.Helpers; + +public class TokenAuthOptions : AuthenticationSchemeOptions +{ + public string Token { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/TokenAuthScheme.cs b/MoonlightServers.Daemon/Helpers/TokenAuthScheme.cs new file mode 100644 index 0000000..0c08b7a --- /dev/null +++ b/MoonlightServers.Daemon/Helpers/TokenAuthScheme.cs @@ -0,0 +1,49 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace MoonlightServers.Daemon.Helpers; + +public class TokenAuthScheme : AuthenticationHandler +{ + public TokenAuthScheme(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + public TokenAuthScheme(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base( + options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + return Task.FromResult(AuthenticateResult.NoResult()); + + var authHeaderValue = Request.Headers["Authorization"].FirstOrDefault(); + + if (string.IsNullOrEmpty(authHeaderValue)) + return Task.FromResult(AuthenticateResult.NoResult()); + + if (!authHeaderValue.Contains("Bearer ")) + return Task.FromResult(AuthenticateResult.NoResult()); + + var providedToken = authHeaderValue + .Replace("Bearer ", "") + .Trim(); + + if (providedToken != Options.Token) + return Task.FromResult(AuthenticateResult.NoResult()); + + return Task.FromResult(AuthenticateResult.Success( + new AuthenticationTicket( + new ClaimsPrincipal( + new ClaimsIdentity("token") + ), + "token" + ) + )); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Services/RemoteService.cs b/MoonlightServers.Daemon/Services/RemoteService.cs index 15a2de4..da6a3cb 100644 --- a/MoonlightServers.Daemon/Services/RemoteService.cs +++ b/MoonlightServers.Daemon/Services/RemoteService.cs @@ -58,37 +58,13 @@ public class RemoteService BaseAddress = new Uri(formattedUrl) }; - var jwt = GenerateJwt(configuration); - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}"); + httpClient.DefaultRequestHeaders.Add( + "Authorization", + $"Bearer {configuration.Security.TokenId}.{configuration.Security.Token}" + ); return new HttpApiClient(httpClient); } - private string GenerateJwt(AppConfiguration configuration) - { - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - - var securityTokenDesc = new SecurityTokenDescriptor() - { - Expires = DateTime.UtcNow.AddYears(1), // TODO: Document somewhere - IssuedAt = DateTime.UtcNow, - Issuer = configuration.Security.TokenId, - Audience = configuration.Remote.Url, - NotBefore = DateTime.UtcNow.AddSeconds(-1), - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(configuration.Security.Token) - ), - SecurityAlgorithms.HmacSha256 - ) - }; - - var securityToken = jwtSecurityTokenHandler.CreateJwtSecurityToken(securityTokenDesc); - - securityToken.Header.Add("kid", configuration.Security.TokenId); - - return jwtSecurityTokenHandler.WriteToken(securityToken); - } - #endregion } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 715937d..1481722 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -1,15 +1,11 @@ -using System.Text; using System.Text.Json; using Docker.DotNet; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using MoonCore.Configuration; using MoonCore.EnvConfiguration; using MoonCore.Extended.Extensions; using MoonCore.Extensions; using MoonCore.Helpers; -using MoonCore.Services; using MoonlightServers.Daemon.Configuration; +using MoonlightServers.Daemon.Helpers; using MoonlightServers.Daemon.Http.Hubs; using MoonlightServers.Daemon.Services; @@ -91,7 +87,7 @@ public class Startup { options.Limits.MaxRequestBodySize = ByteConverter.FromMegaBytes(Configuration.Files.UploadLimit).Bytes; }); - + return Task.CompletedTask; } @@ -137,7 +133,7 @@ public class Startup private async Task SetupAppConfiguration() { var configurationBuilder = new ConfigurationBuilder(); - + // Ensure configuration file exists var jsonFilePath = PathBuilder.File(Directory.GetCurrentDirectory(), "storage", "app.json"); @@ -147,7 +143,7 @@ public class Startup configurationBuilder.AddJsonFile( jsonFilePath ); - + configurationBuilder.AddEnvironmentVariables(prefix: "MOONLIGHT_", separator: "_"); var configurationRoot = configurationBuilder.Build(); @@ -311,32 +307,22 @@ public class Startup private Task RegisterAuth() { WebApplicationBuilder.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => + .AddAuthentication("token") + .AddScheme("token", options => { - options.TokenValidationParameters = new() - { - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( - Configuration.Security.Token - )), - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidateAudience = false, - ValidateIssuer = false, - ClockSkew = TimeSpan.Zero - }; + options.Token = Configuration.Security.Token; }); - + WebApplicationBuilder.Services.AddAuthorization(); - + return Task.CompletedTask; } - + private Task UseAuth() { WebApplication.UseAuthentication(); WebApplication.UseAuthorization(); - + return Task.CompletedTask; }