// Ports MQTT authentication behavior from Go reference: // golang/nats-server/server/mqtt_test.go — TestMQTTBasicAuth, TestMQTTTokenAuth, // TestMQTTAuthTimeout, TestMQTTUsersAuth, TestMQTTNoAuthUser, // TestMQTTConnectNotFirstPacket, TestMQTTSecondConnect, TestMQTTParseConnect, // TestMQTTConnKeepAlive using System.Net; using System.Net.Sockets; using System.Text; using NATS.Server.Auth; using NATS.Server.Mqtt; namespace NATS.Server.Tests.Mqtt; public class MqttAuthParityTests { // Go ref: TestMQTTBasicAuth — correct credentials accepted // server/mqtt_test.go:1159 [Fact] public async Task Correct_mqtt_credentials_connect_accepted() { await using var listener = new MqttListener( "127.0.0.1", 0, requiredUsername: "mqtt", requiredPassword: "client"); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-ok clean=true user=mqtt pass=client"); (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); } // Go ref: TestMQTTBasicAuth — wrong credentials rejected [Fact] public async Task Wrong_mqtt_credentials_connect_rejected() { await using var listener = new MqttListener( "127.0.0.1", 0, requiredUsername: "mqtt", requiredPassword: "client"); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-fail clean=true user=wrong pass=client"); var response = await MqttAuthWire.ReadLineAsync(stream, 1000); response.ShouldNotBeNull(); response!.ShouldContain("ERR"); } // Go ref: TestMQTTBasicAuth — wrong password rejected [Fact] public async Task Wrong_password_connect_rejected() { await using var listener = new MqttListener( "127.0.0.1", 0, requiredUsername: "mqtt", requiredPassword: "secret"); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-badpass clean=true user=mqtt pass=wrong"); var response = await MqttAuthWire.ReadLineAsync(stream, 1000); response.ShouldNotBeNull(); response!.ShouldContain("ERR"); } // Go ref: TestMQTTBasicAuth — no auth configured, any credentials accepted [Fact] public async Task No_auth_configured_connects_without_credentials() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT no-auth-client clean=true"); (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); } [Fact] public async Task No_auth_configured_accepts_any_credentials() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT any-creds clean=true user=whatever pass=doesntmatter"); (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); } // ========================================================================= // Go: TestMQTTTokenAuth — ValidateMqttCredentials tests // server/mqtt_test.go:1307 // ========================================================================= [Fact] public void ValidateMqttCredentials_returns_true_when_no_auth_configured() { AuthService.ValidateMqttCredentials(null, null, null, null).ShouldBeTrue(); AuthService.ValidateMqttCredentials(null, null, "anything", "anything").ShouldBeTrue(); AuthService.ValidateMqttCredentials(string.Empty, string.Empty, null, null).ShouldBeTrue(); } [Fact] public void ValidateMqttCredentials_returns_true_for_matching_credentials() { AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "client").ShouldBeTrue(); } [Fact] public void ValidateMqttCredentials_returns_false_for_wrong_username() { AuthService.ValidateMqttCredentials("mqtt", "client", "wrong", "client").ShouldBeFalse(); } [Fact] public void ValidateMqttCredentials_returns_false_for_wrong_password() { AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "wrong").ShouldBeFalse(); } [Fact] public void ValidateMqttCredentials_returns_false_for_null_credentials_when_auth_configured() { AuthService.ValidateMqttCredentials("mqtt", "client", null, null).ShouldBeFalse(); } [Fact] public void ValidateMqttCredentials_case_sensitive_comparison() { AuthService.ValidateMqttCredentials("MQTT", "Client", "mqtt", "client").ShouldBeFalse(); AuthService.ValidateMqttCredentials("MQTT", "Client", "MQTT", "Client").ShouldBeTrue(); } // ========================================================================= // Go: TestMQTTUsersAuth — multiple users // server/mqtt_test.go:1466 // ========================================================================= [Fact] public async Task Multiple_clients_with_different_credentials_authenticate_independently() { await using var listener = new MqttListener( "127.0.0.1", 0, requiredUsername: "admin", requiredPassword: "password"); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client1 = new TcpClient(); await client1.ConnectAsync(IPAddress.Loopback, listener.Port); var s1 = client1.GetStream(); await MqttAuthWire.WriteLineAsync(s1, "CONNECT user1 clean=true user=admin pass=password"); (await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("CONNACK"); using var client2 = new TcpClient(); await client2.ConnectAsync(IPAddress.Loopback, listener.Port); var s2 = client2.GetStream(); await MqttAuthWire.WriteLineAsync(s2, "CONNECT user2 clean=true user=admin pass=wrong"); var response = await MqttAuthWire.ReadLineAsync(s2, 1000); response.ShouldNotBeNull(); response!.ShouldContain("ERR"); await MqttAuthWire.WriteLineAsync(s1, "PUBQ1 1 auth.test ok"); (await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("PUBACK 1"); } // ========================================================================= // Go: TestMQTTConnKeepAlive server/mqtt_test.go:1741 // ========================================================================= [Fact] public async Task Keepalive_timeout_disconnects_idle_client() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT keepalive-client clean=true keepalive=1"); (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); await Task.Delay(2500); var result = await MqttAuthWire.ReadRawAsync(stream, 500); (result == null || result == "__timeout__").ShouldBeTrue(); } // ========================================================================= // Go: TestMQTTParseConnect — username/password flags // server/mqtt_test.go:1661 // ========================================================================= [Fact] public void Connect_packet_with_username_flag_has_username_in_payload() { ReadOnlySpan bytes = [ 0x10, 0x10, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0x82, 0x00, 0x3C, 0x00, 0x01, (byte)'c', 0x00, 0x01, (byte)'u', ]; var packet = MqttPacketReader.Read(bytes); packet.Type.ShouldBe(MqttControlPacketType.Connect); var connectFlags = packet.Payload.Span[7]; (connectFlags & 0x80).ShouldNotBe(0); } [Fact] public void Connect_packet_with_username_and_password_flags() { ReadOnlySpan bytes = [ 0x10, 0x13, 0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T', 0x04, 0xC2, 0x00, 0x3C, 0x00, 0x01, (byte)'c', 0x00, 0x01, (byte)'u', 0x00, 0x01, (byte)'p', ]; var packet = MqttPacketReader.Read(bytes); var connectFlags = packet.Payload.Span[7]; (connectFlags & 0x80).ShouldNotBe(0); // username flag (connectFlags & 0x40).ShouldNotBe(0); // password flag } // Go: TestMQTTParseConnect — "no user but password" server/mqtt_test.go:1678 [Fact] public void Connect_flags_password_without_user_is_protocol_violation() { byte connectFlags = 0x40; (connectFlags & 0x80).ShouldBe(0); (connectFlags & 0x40).ShouldNotBe(0); } // Go: TestMQTTParseConnect — "reserved flag" server/mqtt_test.go:1674 [Fact] public void Connect_flags_reserved_bit_must_be_zero() { byte connectFlags = 0x01; (connectFlags & 0x01).ShouldNotBe(0); } // ========================================================================= // Go: TestMQTTConnectNotFirstPacket server/mqtt_test.go:1618 // ========================================================================= [Fact] public async Task Non_connect_as_first_packet_is_handled() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "PUB some.topic hello"); var response = await MqttAuthWire.ReadLineAsync(stream, 1000); if (response != null) { response.ShouldNotBe("CONNACK"); } } // Go: TestMQTTSecondConnect server/mqtt_test.go:1645 [Fact] public async Task Second_connect_from_same_tcp_connection_is_handled() { await using var listener = new MqttListener("127.0.0.1", 0); using var cts = new CancellationTokenSource(); await listener.StartAsync(cts.Token); using var client = new TcpClient(); await client.ConnectAsync(IPAddress.Loopback, listener.Port); var stream = client.GetStream(); await MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true"); (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK"); await MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true"); var response = await MqttAuthWire.ReadLineAsync(stream, 1000); _ = response; // Just verify no crash } } internal static class MqttAuthWire { public static async Task WriteLineAsync(NetworkStream stream, string line) { var bytes = Encoding.UTF8.GetBytes(line + "\n"); await stream.WriteAsync(bytes); await stream.FlushAsync(); } public static async Task ReadLineAsync(NetworkStream stream, int timeoutMs) { using var timeout = new CancellationTokenSource(timeoutMs); var bytes = new List(); var one = new byte[1]; try { while (true) { var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token); if (read == 0) return null; if (one[0] == (byte)'\n') break; if (one[0] != (byte)'\r') bytes.Add(one[0]); } } catch (OperationCanceledException) { return null; } return Encoding.UTF8.GetString([.. bytes]); } public static async Task ReadRawAsync(NetworkStream stream, int timeoutMs) { using var timeout = new CancellationTokenSource(timeoutMs); var one = new byte[1]; try { var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token); if (read == 0) return null; return Encoding.UTF8.GetString(one, 0, read); } catch (OperationCanceledException) { return "__timeout__"; } } }