Implemented proper node authentication

This commit is contained in:
2025-04-11 22:58:00 +02:00
parent f0948960b7
commit ec0c336825
13 changed files with 174 additions and 165 deletions

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.ApiServer.Helpers;
public class NodeAuthOptions : AuthenticationSchemeOptions
{
}

View File

@@ -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<NodeAuthOptions>
{
public NodeAuthScheme(IOptionsMonitor<NodeAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
public NodeAuthScheme(IOptionsMonitor<NodeAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(
options, logger, encoder)
{
}
protected override async Task<AuthenticateResult> 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<DatabaseRepository<Node>>();
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"
)
);
}
}

View File

@@ -48,8 +48,8 @@ public class NodesController : Controller
{
var node = Mapper.Map<Node>(request);
node.Token = Formatter.GenerateString(32);
node.TokenId = Formatter.GenerateString(6);
node.Token = Formatter.GenerateString(32);
var finalNode = await NodeRepository.Add(node);

View File

@@ -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")]

View File

@@ -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<Server> ServerRepository;
@@ -21,7 +21,8 @@ public class ServersController : Controller
public ServersController(
DatabaseRepository<Server> serverRepository,
DatabaseRepository<Node> nodeRepository,
ILogger<ServersController> logger)
ILogger<ServersController> logger
)
{
ServerRepository = serverRepository;
NodeRepository = nodeRepository;
@@ -31,12 +32,12 @@ public class ServersController : Controller
[HttpGet]
public async Task<PagedData<ServerDataResponse>> 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<ServerDataResponse> 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<ServerInstallDataResponse> 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

View File

@@ -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<JwtBearerOptions>
{
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<DatabaseRepository<Node>>();
var node = nodeRepo
.Get()
.FirstOrDefault(x => x.TokenId == kid);
if (node == null)
return [];
return
[
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(node.Token)
)
];
};
}
}

View File

@@ -21,8 +21,8 @@
<ItemGroup>
<Folder Include="Database\Migrations\"/>
<Folder Include="Helpers\"/>
<Folder Include="Http\Middleware\"/>
<Folder Include="Implementations\" />
<Folder Include="Interfaces\"/>
</ItemGroup>

View File

@@ -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);
}

View File

@@ -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<IConfigureOptions<JwtBearerOptions>, NodeJwtBearerOptions>();
.AddScheme<NodeAuthOptions, NodeAuthScheme>("nodeAuthentication", null);
return Task.CompletedTask;
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace MoonlightServers.Daemon.Helpers;
public class TokenAuthOptions : AuthenticationSchemeOptions
{
public string Token { get; set; }
}

View File

@@ -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<TokenAuthOptions>
{
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
public TokenAuthScheme(IOptionsMonitor<TokenAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(
options, logger, encoder)
{
}
protected override Task<AuthenticateResult> 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"
)
));
}
}

View File

@@ -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
}

View File

@@ -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<TokenAuthOptions, TokenAuthScheme>("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;
}