Wire AuthService into NatsServer and NatsClient to enforce authentication on incoming connections. The server builds an AuthService from NatsOptions, sets auth_required in ServerInfo, and generates per-client nonces when NKey auth is configured. NatsClient validates credentials in ProcessConnect, enforces publish/subscribe permissions, and implements an auth timeout that closes connections that don't send CONNECT in time. Existing tests without auth continue to work since AuthService.IsAuthRequired is false by default.
257 lines
7.0 KiB
C#
257 lines
7.0 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Client.Core;
|
|
using NATS.Server;
|
|
using NATS.Server.Auth;
|
|
|
|
namespace NATS.Server.Tests;
|
|
|
|
public class AuthIntegrationTests
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether any exception in the chain contains the given substring.
|
|
/// The NATS client wraps server errors in outer NatsException messages,
|
|
/// so the actual "Authorization Violation" may be in an inner exception.
|
|
/// </summary>
|
|
private static bool ExceptionChainContains(Exception ex, string substring)
|
|
{
|
|
Exception? current = ex;
|
|
while (current != null)
|
|
{
|
|
if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
current = current.InnerException;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static (NatsServer server, int port, CancellationTokenSource cts) StartServer(NatsOptions options)
|
|
{
|
|
var port = GetFreePort();
|
|
options.Port = port;
|
|
var server = new NatsServer(options, NullLoggerFactory.Instance);
|
|
var cts = new CancellationTokenSource();
|
|
_ = server.StartAsync(cts.Token);
|
|
return (server, port, cts);
|
|
}
|
|
|
|
private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
|
|
{
|
|
var (server, port, cts) = StartServer(options);
|
|
await server.WaitForReadyAsync();
|
|
return (server, port, cts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Token_auth_success()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Authorization = "s3cr3t",
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://s3cr3t@127.0.0.1:{port}",
|
|
});
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Token_auth_failure_disconnects()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Authorization = "s3cr3t",
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://wrongtoken@127.0.0.1:{port}",
|
|
MaxReconnectRetry = 0,
|
|
});
|
|
|
|
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
|
{
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
});
|
|
|
|
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
|
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UserPassword_auth_success()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Username = "admin",
|
|
Password = "secret",
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://admin:secret@127.0.0.1:{port}",
|
|
});
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UserPassword_auth_failure_disconnects()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Username = "admin",
|
|
Password = "secret",
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://admin:wrong@127.0.0.1:{port}",
|
|
MaxReconnectRetry = 0,
|
|
});
|
|
|
|
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
|
{
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
});
|
|
|
|
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
|
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MultiUser_auth_success()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Users =
|
|
[
|
|
new User { Username = "alice", Password = "pass1" },
|
|
new User { Username = "bob", Password = "pass2" },
|
|
],
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var alice = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://alice:pass1@127.0.0.1:{port}",
|
|
});
|
|
await using var bob = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://bob:pass2@127.0.0.1:{port}",
|
|
});
|
|
|
|
await alice.ConnectAsync();
|
|
await alice.PingAsync();
|
|
|
|
await bob.ConnectAsync();
|
|
await bob.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task No_credentials_when_auth_required_disconnects()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions
|
|
{
|
|
Authorization = "s3cr3t",
|
|
});
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{port}",
|
|
MaxReconnectRetry = 0,
|
|
});
|
|
|
|
var ex = await Should.ThrowAsync<NatsException>(async () =>
|
|
{
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
});
|
|
|
|
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
|
|
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task No_auth_configured_allows_all()
|
|
{
|
|
var (server, port, cts) = await StartServerAsync(new NatsOptions());
|
|
|
|
try
|
|
{
|
|
await using var client = new NatsConnection(new NatsOpts
|
|
{
|
|
Url = $"nats://127.0.0.1:{port}",
|
|
});
|
|
await client.ConnectAsync();
|
|
await client.PingAsync();
|
|
}
|
|
finally
|
|
{
|
|
await cts.CancelAsync();
|
|
server.Dispose();
|
|
}
|
|
}
|
|
}
|