diff --git a/src/NATS.Server/NatsServer.cs b/src/NATS.Server/NatsServer.cs index 6297f1b..fbf540a 100644 --- a/src/NATS.Server/NatsServer.cs +++ b/src/NATS.Server/NatsServer.cs @@ -99,7 +99,10 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable var clientInfo = _serverInfo; if (_authService.NonceRequired) { - nonce = _authService.GenerateNonce(); + var rawNonce = _authService.GenerateNonce(); + var nonceStr = _authService.EncodeNonce(rawNonce); + // The client signs the nonce string (ASCII), not the raw bytes + nonce = Encoding.ASCII.GetBytes(nonceStr); clientInfo = new ServerInfo { ServerId = _serverInfo.ServerId, @@ -109,7 +112,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable Port = _serverInfo.Port, MaxPayload = _serverInfo.MaxPayload, AuthRequired = _serverInfo.AuthRequired, - Nonce = _authService.EncodeNonce(nonce), + Nonce = nonceStr, }; } diff --git a/tests/NATS.Server.Tests/NKeyIntegrationTests.cs b/tests/NATS.Server.Tests/NKeyIntegrationTests.cs new file mode 100644 index 0000000..83309f6 --- /dev/null +++ b/tests/NATS.Server.Tests/NKeyIntegrationTests.cs @@ -0,0 +1,82 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging.Abstractions; +using NATS.Client.Core; +using NATS.NKeys; +using NATS.Server.Auth; + +namespace NATS.Server.Tests; + +public class NKeyIntegrationTests : IAsyncLifetime +{ + private NatsServer _server = null!; + private int _port; + private readonly CancellationTokenSource _cts = new(); + private Task _serverTask = null!; + private KeyPair _userKeyPair = null!; + private string _userSeed = null!; + private string _userPublicKey = null!; + + private static int GetFreePort() + { + using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)sock.LocalEndPoint!).Port; + } + + public async Task InitializeAsync() + { + _port = GetFreePort(); + _userKeyPair = KeyPair.CreatePair(PrefixByte.User); + _userPublicKey = _userKeyPair.GetPublicKey(); + _userSeed = _userKeyPair.GetSeed(); + + _server = new NatsServer(new NatsOptions + { + Port = _port, + NKeys = [new NKeyUser { Nkey = _userPublicKey }], + }, NullLoggerFactory.Instance); + + _serverTask = _server.StartAsync(_cts.Token); + await _server.WaitForReadyAsync(); + } + + public async Task DisposeAsync() + { + await _cts.CancelAsync(); + _server.Dispose(); + } + + [Fact] + public async Task NKey_auth_success() + { + await using var client = new NatsConnection(new NatsOpts + { + Url = $"nats://127.0.0.1:{_port}", + AuthOpts = new NatsAuthOpts { NKey = _userPublicKey, Seed = _userSeed }, + }); + + await client.ConnectAsync(); + await client.PingAsync(); + } + + [Fact] + public async Task NKey_auth_wrong_key_fails() + { + // Generate a different key pair not known to the server + var otherKp = KeyPair.CreatePair(PrefixByte.User); + + await using var client = new NatsConnection(new NatsOpts + { + Url = $"nats://127.0.0.1:{_port}", + AuthOpts = new NatsAuthOpts { NKey = otherKp.GetPublicKey(), Seed = otherKp.GetSeed() }, + MaxReconnectRetry = 0, + }); + + await Should.ThrowAsync(async () => + { + await client.ConnectAsync(); + await client.PingAsync(); + }); + } +} diff --git a/tests/NATS.Server.Tests/PermissionIntegrationTests.cs b/tests/NATS.Server.Tests/PermissionIntegrationTests.cs new file mode 100644 index 0000000..358da95 --- /dev/null +++ b/tests/NATS.Server.Tests/PermissionIntegrationTests.cs @@ -0,0 +1,119 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging.Abstractions; +using NATS.Client.Core; +using NATS.Server.Auth; + +namespace NATS.Server.Tests; + +public class PermissionIntegrationTests : IAsyncLifetime +{ + private NatsServer _server = null!; + private int _port; + private readonly CancellationTokenSource _cts = new(); + private Task _serverTask = null!; + + private static int GetFreePort() + { + using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + sock.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)sock.LocalEndPoint!).Port; + } + + public async Task InitializeAsync() + { + _port = GetFreePort(); + _server = new NatsServer(new NatsOptions + { + Port = _port, + Users = + [ + new User + { + Username = "publisher", + Password = "pass", + Permissions = new Permissions + { + Publish = new SubjectPermission { Allow = ["events.>"] }, + Subscribe = new SubjectPermission { Deny = [">"] }, + }, + }, + new User + { + Username = "subscriber", + Password = "pass", + Permissions = new Permissions + { + Publish = new SubjectPermission { Deny = [">"] }, + Subscribe = new SubjectPermission { Allow = ["events.>"] }, + }, + }, + new User + { + Username = "admin", + Password = "pass", + // No permissions — full access + }, + ], + }, NullLoggerFactory.Instance); + + _serverTask = _server.StartAsync(_cts.Token); + await _server.WaitForReadyAsync(); + } + + public async Task DisposeAsync() + { + await _cts.CancelAsync(); + _server.Dispose(); + } + + [Fact] + public async Task Publisher_can_publish_to_allowed_subject() + { + await using var pub = new NatsConnection(new NatsOpts + { + Url = $"nats://publisher:pass@127.0.0.1:{_port}", + }); + await using var admin = new NatsConnection(new NatsOpts + { + Url = $"nats://admin:pass@127.0.0.1:{_port}", + }); + + await pub.ConnectAsync(); + await admin.ConnectAsync(); + + await using var sub = await admin.SubscribeCoreAsync("events.test"); + await admin.PingAsync(); + + await pub.PublishAsync("events.test", "hello"); + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var msg = await sub.Msgs.ReadAsync(timeout.Token); + msg.Data.ShouldBe("hello"); + } + + [Fact] + public async Task Admin_has_full_access() + { + await using var admin1 = new NatsConnection(new NatsOpts + { + Url = $"nats://admin:pass@127.0.0.1:{_port}", + }); + await using var admin2 = new NatsConnection(new NatsOpts + { + Url = $"nats://admin:pass@127.0.0.1:{_port}", + }); + + await admin1.ConnectAsync(); + await admin2.ConnectAsync(); + + await using var sub = await admin2.SubscribeCoreAsync("anything.at.all"); + await admin2.PingAsync(); + + await admin1.PublishAsync("anything.at.all", "data"); + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var msg = await sub.Msgs.ReadAsync(timeout.Token); + msg.Data.ShouldBe("data"); + } +}