From 9dbe1e4b123e60224c21f2a46263482a6f6e24b2 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:17:20 +0100 Subject: [PATCH] Fix support for enhanced authentication (#2132) * Implement enhanced authentication for client and server. * Update samples * Add abstraction layer * Merge master * Use dedicated class for public API. * Update release notes * Rename methods to fit naming scheme * Fix unit tests and samples --- Samples/Client/Client_Connection_Samples.cs | 525 ++++++++++-------- .../ExchangeEnhancedAuthenticationOptions.cs | 16 + ...ngeEnhancedAuthenticationOptionsFactory.cs | 70 +++ .../ExchangeEnhancedAuthenticationResult.cs | 12 + ...angeEnhancedAuthenticationResultFactory.cs | 34 ++ .../Events/ValidatingConnectionEventArgs.cs | 394 +++++++------ .../Internal/MqttClientSessionsManager.cs | 6 +- .../Internal/MqttRetainedMessagesManager.cs | 1 + Source/MQTTnet.Server/MqttServerFactory.cs | 6 + .../MqttClient/MqttClient_Connection_Tests.cs | 286 ++++++---- Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 1 + ...ttExtendedAuthenticationExchangeContext.cs | 63 --- .../Formatter/ReadFixedHeaderResult.cs | 35 +- .../Formatter/V5/MqttV5PacketDecoder.cs | 2 +- Source/MQTTnet/IMqttClient.cs | 2 +- Source/MQTTnet/MqttClient.cs | 65 ++- Source/MQTTnet/MqttClientExtensions.cs | 4 +- .../IMqttEnhancedAuthenticationHandler.cs} | 4 +- Source/MQTTnet/Options/MqttClientOptions.cs | 13 +- .../Options/MqttClientOptionsBuilder.cs | 40 +- .../Options/MqttClientOptionsValidator.cs | 5 + .../MqttEnhancedAuthenticationEventArgs.cs | 108 ++++ ...MqttEnhancedAuthenticationExchangeData.cs} | 2 +- ...iveMqttEnhancedAuthenticationDataResult.cs | 22 + ...ndMqttEnhancedAuthenticationDataOptions.cs | 17 + Source/ReleaseNotes.md | 4 + 26 files changed, 1071 insertions(+), 666 deletions(-) create mode 100644 Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptions.cs create mode 100644 Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs create mode 100644 Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResult.cs create mode 100644 Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResultFactory.cs delete mode 100644 Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs rename Source/MQTTnet/{ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs => Options/IMqttEnhancedAuthenticationHandler.cs} (64%) create mode 100644 Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs rename Source/MQTTnet/{ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs => Options/MqttEnhancedAuthenticationExchangeData.cs} (97%) create mode 100644 Source/MQTTnet/Options/ReceiveMqttEnhancedAuthenticationDataResult.cs create mode 100644 Source/MQTTnet/Options/SendMqttEnhancedAuthenticationDataOptions.cs diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs index ee1ee20fb..8d716f500 100644 --- a/Samples/Client/Client_Connection_Samples.cs +++ b/Samples/Client/Client_Connection_Samples.cs @@ -8,36 +8,65 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Text; using MQTTnet.Formatter; +using MQTTnet.Protocol; using MQTTnet.Samples.Helpers; +using MQTTnet.Server; +using MQTTnet.Server.EnhancedAuthentication; namespace MQTTnet.Samples.Client; public static class Client_Connection_Samples { + const string mosquitto_org = @" +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL +BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG +A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU +BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv +by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE +BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES +MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp +dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg +UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW +Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA +s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH +3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo +E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT +MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV +6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC +6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf ++pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK +sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 +LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE +m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= +-----END CERTIFICATE----- +"; + public static async Task Clean_Disconnect() { /* * This sample disconnects in a clean way. This will send a MQTT DISCONNECT packet - * to the server and close the connection afterwards. + * to the server and close the connection afterward. * * See sample _Connect_Client_ for more details. */ var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - // This will send the DISCONNECT packet. Calling _Dispose_ without DisconnectAsync the - // connection is closed in a "not clean" way. See MQTT specification for more details. - await mqttClient.DisconnectAsync(new MqttClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.NormalDisconnection).Build()); - } + // This will send the DISCONNECT packet. Calling _Dispose_ without DisconnectAsync the + // connection is closed in a "not clean" way. See MQTT specification for more details. + await mqttClient.DisconnectAsync(new MqttClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.NormalDisconnection).Build()); } - public static async Task Connect_Client() + public static async Task Connect() { /* * This sample creates a simple MQTT client and connects to a public broker. @@ -48,57 +77,93 @@ public static async Task Connect_Client() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - // Use builder classes where possible in this project. - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + // Use builder classes where possible in this project. + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - // This will throw an exception if the server is not available. - // The result from this message returns additional data which was sent - // from the server. Please refer to the MQTT protocol specification for details. - var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + // This will throw an exception if the server is not available. + // The result from this message returns additional data which was sent + // from the server. Please refer to the MQTT protocol specification for details. + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("The MQTT client is connected."); + Console.WriteLine("The MQTT client is connected."); - response.DumpToConsole(); + response.DumpToConsole(); - // Send a clean disconnect to the server by calling _DisconnectAsync_. Without this the TCP connection - // gets dropped and the server will handle this as a non clean disconnect (see MQTT spec for details). - var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder().Build(); + // Send a clean disconnect to the server by calling _DisconnectAsync_. Without this the TCP connection + // gets dropped and the server will handle this as a non clean disconnect (see MQTT spec for details). + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder().Build(); - await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); - } + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); } - public static async Task Connect_Client_Timeout() + public static async Task Connect_Using_Enhanced_Authentication() { /* - * This sample creates a simple MQTT client and connects to an invalid broker using a timeout. - * - * This is a modified version of the sample _Connect_Client_! See other sample for more details. + * This sample uses enhanced authentication (Kerberos) when creating the connection. */ - var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build(); + /* + * Server part... + */ - try + var mqttServerFactory = new MqttServerFactory(); + + var serverOptions = mqttServerFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build(); + var server = mqttServerFactory.CreateMqttServer(serverOptions); + + server.ValidatingConnectionAsync += async args => + { + if (args.AuthenticationMethod == "GS2-KRB5") { - using (var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(1))) - { - await mqttClient.ConnectAsync(mqttClientOptions, timeoutToken.Token); - } + var result = await args.ExchangeEnhancedAuthenticationAsync(new ExchangeEnhancedAuthenticationOptions(), args.CancellationToken); + + Console.WriteLine($"Received AUTH data from client: {Encoding.UTF8.GetString(result.AuthenticationData)}"); + + var authOptions = mqttServerFactory.CreateExchangeExtendedAuthenticationOptionsBuilder().WithAuthenticationData("reply context token").Build(); + + result = await args.ExchangeEnhancedAuthenticationAsync(authOptions, args.CancellationToken); + + Console.WriteLine($"Received AUTH data from client: {Encoding.UTF8.GetString(result.AuthenticationData)}"); + + args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray(); + + // Authentication DONE! + args.ReasonCode = MqttConnectReasonCode.Success; // Also the default! } - catch (OperationCanceledException) + else { - Console.WriteLine("Timeout while connecting."); + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; } - } + }; + + await server.StartAsync(); + + /* + * Client part... + */ + + var mqttClientFactory = new MqttClientFactory(); + + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new SampleClientKerberosAuthenticationHandler(); + + var clientOptions = mqttClientFactory.CreateClientOptionsBuilder() + .WithTcpServer("localhost") + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithEnhancedAuthenticationHandler(kerberosAuthenticationHandler) + .WithEnhancedAuthentication("GS2-KRB5") + .Build(); + + var client = mqttClientFactory.CreateMqttClient(); + + var result = await client.ConnectAsync(clientOptions); + + Console.WriteLine($"Client connect result: {result.ResultCode}"); } - public static async Task Connect_Client_Using_MQTTv5() + public static async Task Connect_Using_MQTTv5() { /* * This sample creates a simple MQTT client and connects to a public broker using MQTTv5. @@ -108,20 +173,18 @@ public static async Task Connect_Client_Using_MQTTv5() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").WithProtocolVersion(MqttProtocolVersion.V500).Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").WithProtocolVersion(MqttProtocolVersion.V500).Build(); - // In MQTTv5 the response contains much more information. - var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + // In MQTTv5 the response contains much more information. + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("The MQTT client is connected."); + Console.WriteLine("The MQTT client is connected."); - response.DumpToConsole(); - } + response.DumpToConsole(); } - public static async Task Connect_Client_Using_TLS_1_2() + public static async Task Connect_Using_TLS_1_2() { /* * This sample creates a simple MQTT client and connects to a public broker using TLS 1.2 encryption. @@ -131,82 +194,88 @@ public static async Task Connect_Client_Using_TLS_1_2() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io") - .WithTlsOptions( - o => - { - // The used public broker sometimes has invalid certificates. This sample accepts all - // certificates. This should not be used in live environments. - o.WithCertificateValidationHandler(_ => true); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io") + .WithTlsOptions( + o => + { + // The used public broker sometimes has invalid certificates. This sample accepts all + // certificates. This should not be used in live environments. + o.WithCertificateValidationHandler(_ => true); - // The default value is determined by the OS. Set manually to force version. - o.WithSslProtocols(SslProtocols.Tls12); - }) - .Build(); + // The default value is determined by the OS. Set manually to force version. + o.WithSslProtocols(SslProtocols.Tls12); + }) + .Build(); - using (var timeout = new CancellationTokenSource(5000)) - { - await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); + using var timeout = new CancellationTokenSource(5000); + await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); - Console.WriteLine("The MQTT client is connected."); - } - } + Console.WriteLine("The MQTT client is connected."); } - public static async Task Connect_Client_Using_WebSockets() + public static async Task Connect_Using_TLS_Encryption() { /* - * This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection. + * This sample creates a simple MQTT client and connects to a public broker with enabled TLS encryption. * * This is a modified version of the sample _Connect_Client_! See other sample for more details. */ var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) + .WithTlsOptions( + o => o.WithCertificateValidationHandler( + // The used public broker sometimes has invalid certificates. This sample accepts all + // certificates. This should not be used in live environments. + _ => true)) + .Build(); - var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + // In MQTTv5 the response contains much more information. + using var timeout = new CancellationTokenSource(5000); + var response = await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); - Console.WriteLine("The MQTT client is connected."); + Console.WriteLine("The MQTT client is connected."); - response.DumpToConsole(); - } + response.DumpToConsole(); + } + + public static async Task Connect_Using_TLS_With_CA_File() + { + var mqttFactory = new MqttClientFactory(); + + var caChain = new X509Certificate2Collection(); + caChain.ImportFromPem(mosquitto_org); // from https://test.mosquitto.org/ssl/mosquitto.org.crt + + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) + .WithTlsOptions(new MqttClientTlsOptionsBuilder().WithTrustChain(caChain).Build()) + .Build(); + + var connAck = await mqttClient.ConnectAsync(mqttClientOptions); + Console.WriteLine("Connected to test.moquitto.org:8883 with CaFile mosquitto.org.crt: " + connAck.ResultCode); } - public static async Task Connect_Client_With_TLS_Encryption() + public static async Task Connect_Using_WebSockets() { /* - * This sample creates a simple MQTT client and connects to a public broker with enabled TLS encryption. + * This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection. * * This is a modified version of the sample _Connect_Client_! See other sample for more details. */ var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) - .WithTlsOptions( - o => o.WithCertificateValidationHandler( - // The used public broker sometimes has invalid certificates. This sample accepts all - // certificates. This should not be used in live environments. - _ => true)) - .Build(); - - // In MQTTv5 the response contains much more information. - using (var timeout = new CancellationTokenSource(5000)) - { - var response = await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer(o => o.WithUri("broker.hivemq.com:8000/mqtt")).Build(); - Console.WriteLine("The MQTT client is connected."); + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - response.DumpToConsole(); - } - } + Console.WriteLine("The MQTT client is connected."); + + response.DumpToConsole(); } public static async Task Connect_With_Amazon_AWS() @@ -219,19 +288,17 @@ public static async Task Connect_With_Amazon_AWS() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("amazon.web.services.broker") - // Disabling packet fragmentation is very important! - .WithoutPacketFragmentation() - .Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("amazon.web.services.broker") + // Disabling packet fragmentation is very important! + .WithoutPacketFragmentation() + .Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - Console.WriteLine("The MQTT client is connected."); + Console.WriteLine("The MQTT client is connected."); - await mqttClient.DisconnectAsync(); - } + await mqttClient.DisconnectAsync(); } public static async Task Disconnect_Clean() @@ -244,23 +311,21 @@ public static async Task Disconnect_Clean() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - // Calling _DisconnectAsync_ will send a DISCONNECT packet before closing the connection. - // Using a reason code requires MQTT version 5.0.0! - await mqttClient.DisconnectAsync(MqttClientDisconnectOptionsReason.ImplementationSpecificError); - } + // Calling _DisconnectAsync_ will send a DISCONNECT packet before closing the connection. + // Using a reason code requires MQTT version 5.0.0! + await mqttClient.DisconnectAsync(MqttClientDisconnectOptionsReason.ImplementationSpecificError); } public static async Task Disconnect_Non_Clean() { /* * This sample disconnects from the server without sending a DISCONNECT packet. - * This way of disconnecting is treated as a non clean disconnect which will + * This way of disconnecting is treated as a non-clean disconnect which will * trigger sending the last will etc. */ @@ -286,31 +351,27 @@ public static async Task Inspect_Certificate_Validation_Errors() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io", 8883) - .WithTlsOptions( - o => - { - o.WithCertificateValidationHandler( - eventArgs => - { - eventArgs.Certificate.Subject.DumpToConsole(); - eventArgs.Certificate.GetExpirationDateString().DumpToConsole(); - eventArgs.Chain.ChainPolicy.RevocationMode.DumpToConsole(); - eventArgs.Chain.ChainStatus.DumpToConsole(); - eventArgs.SslPolicyErrors.DumpToConsole(); - return true; - }); - }) - .Build(); - - // In MQTTv5 the response contains much more information. - using (var timeout = new CancellationTokenSource(5000)) - { - await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); - } - } + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io", 8883) + .WithTlsOptions( + o => + { + o.WithCertificateValidationHandler( + eventArgs => + { + eventArgs.Certificate.Subject.DumpToConsole(); + eventArgs.Certificate.GetExpirationDateString().DumpToConsole(); + eventArgs.Chain.ChainPolicy.RevocationMode.DumpToConsole(); + eventArgs.Chain.ChainStatus.DumpToConsole(); + eventArgs.SslPolicyErrors.DumpToConsole(); + return true; + }); + }) + .Build(); + + // In MQTTv5 the response contains much more information. + using var timeout = new CancellationTokenSource(5000); + await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token); } public static async Task Ping_Server() @@ -323,17 +384,15 @@ public static async Task Ping_Server() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - // This will throw an exception if the server does not reply. - await mqttClient.PingAsync(CancellationToken.None); + // This will throw an exception if the server does not reply. + await mqttClient.PingAsync(CancellationToken.None); - Console.WriteLine("The MQTT server replied to the ping request."); - } + Console.WriteLine("The MQTT server replied to the ping request."); } public static async Task Reconnect_Using_Event() @@ -341,26 +400,24 @@ public static async Task Reconnect_Using_Event() /* * This sample shows how to reconnect when the connection was dropped. * This approach uses one of the events from the client. - * This approach has a risk of dead locks! Consider using the timer approach (see sample). + * This approach has a risk of deadlocks! Consider using the timer approach (see sample). */ var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - mqttClient.DisconnectedAsync += async e => + mqttClient.DisconnectedAsync += async e => + { + if (e.ClientWasConnected) { - if (e.ClientWasConnected) - { - // Use the current options as the new options. - await mqttClient.ConnectAsync(mqttClient.Options); - } - }; + // Use the current options as the new options. + await mqttClient.ConnectAsync(mqttClient.Options); + } + }; - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - } + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); } public static void Reconnect_Using_Timer() @@ -373,91 +430,93 @@ public static void Reconnect_Using_Timer() var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); - _ = Task.Run( - async () => + _ = Task.Run( + async () => + { + // User proper cancellation and no while(true). + while (true) { - // User proper cancellation and no while(true). - while (true) + try { - try + // This code will also do the very first connect! So no call to _ConnectAsync_ is required in the first place. + if (!await mqttClient.TryPingAsync()) { - // This code will also do the very first connect! So no call to _ConnectAsync_ is required in the first place. - if (!await mqttClient.TryPingAsync()) - { - await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); - - // Subscribe to topics when session is clean etc. - Console.WriteLine("The MQTT client is connected."); - } - } - catch - { - // Handle the exception properly (logging etc.). - } - finally - { - // Check the connection state every 5 seconds and perform a reconnect if required. - await Task.Delay(TimeSpan.FromSeconds(5)); + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // Subscribe to topics when session is clean etc. + Console.WriteLine("The MQTT client is connected."); } } - }); + catch + { + // Handle the exception properly (logging etc.). + } + finally + { + // Check the connection state every 5 seconds and perform a reconnect if required. + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + }); - Console.WriteLine("Press to exit"); - Console.ReadLine(); - } + Console.WriteLine("Press to exit"); + Console.ReadLine(); } - public static async Task ConnectTls_WithCaFile() + public static async Task Timeout() { + /* + * This sample creates a simple MQTT client and connects to an invalid broker using a timeout. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + var mqttFactory = new MqttClientFactory(); - X509Certificate2Collection caChain = new X509Certificate2Collection(); - caChain.ImportFromPem(mosquitto_org); // from https://test.mosquitto.org/ssl/mosquitto.org.crt + using var mqttClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build(); - using (var mqttClient = mqttFactory.CreateMqttClient()) + try + { + using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + await mqttClient.ConnectAsync(mqttClientOptions, timeoutToken.Token); + } + catch (OperationCanceledException) { - var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("test.mosquitto.org", 8883) - .WithTlsOptions(new MqttClientTlsOptionsBuilder() - .WithTrustChain(caChain) - .Build()) - .Build(); - - var connAck = await mqttClient.ConnectAsync(mqttClientOptions); - Console.WriteLine("Connected to test.moquitto.org:8883 with CaFile mosquitto.org.crt: " + connAck.ResultCode); + Console.WriteLine("Timeout while connecting."); } + } + class SampleClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler + { + public async Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs) + { + if (eventArgs.AuthenticationMethod != "GS2-KRB5") + { + throw new InvalidOperationException("Wrong authentication method"); + } - } - const string mosquitto_org = @" ------BEGIN CERTIFICATE----- -MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL -BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG -A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU -BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv -by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE -BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES -MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp -dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg -UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW -Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA -s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH -3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo -E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT -MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV -6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC -6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf -+pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK -sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 -LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE -m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= ------END CERTIFICATE----- -"; + var sendOptions = new SendMqttEnhancedAuthenticationDataOptions + { + Data = "initial context token"u8.ToArray() + }; + + await eventArgs.SendAsync(sendOptions); + + var response = await eventArgs.ReceiveAsync(CancellationToken.None); + + Console.WriteLine($"Received AUTH data from server: {Encoding.UTF8.GetString(response.AuthenticationData)}"); + // No further data is required, but we have to fulfil the exchange. + sendOptions = new SendMqttEnhancedAuthenticationDataOptions + { + Data = [] + }; + + await eventArgs.SendAsync(sendOptions, CancellationToken.None); + } + } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptions.cs b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptions.cs new file mode 100644 index 000000000..d832a96dd --- /dev/null +++ b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Packets; + +namespace MQTTnet.Server.EnhancedAuthentication; + +public sealed class ExchangeEnhancedAuthenticationOptions +{ + public byte[] AuthenticationData { get; set; } + + public string ReasonString { get; set; } + + public List UserProperties { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs new file mode 100644 index 000000000..1b55e3b4e --- /dev/null +++ b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationOptionsFactory.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text; +using MQTTnet.Packets; + +namespace MQTTnet.Server.EnhancedAuthentication; + +public sealed class ExchangeEnhancedAuthenticationOptionsFactory +{ + readonly ExchangeEnhancedAuthenticationOptions _options = new(); + + public ExchangeEnhancedAuthenticationOptions Build() + { + return _options; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithAuthenticationData(byte[] authenticationData) + { + _options.AuthenticationData = authenticationData; + + return this; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithAuthenticationData(string authenticationData) + { + if (authenticationData == null) + { + _options.AuthenticationData = null; + } + else + { + _options.AuthenticationData = Encoding.UTF8.GetBytes(authenticationData); + } + + return this; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithReasonString(string reasonString) + { + _options.ReasonString = reasonString; + + return this; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperties(List userProperties) + { + _options.UserProperties = userProperties; + + return this; + } + + public ExchangeEnhancedAuthenticationOptionsFactory WithUserProperty(string name, string value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResult.cs b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResult.cs new file mode 100644 index 000000000..9b9d6ba96 --- /dev/null +++ b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResult.cs @@ -0,0 +1,12 @@ +using MQTTnet.Packets; + +namespace MQTTnet.Server.EnhancedAuthentication; + +public sealed class ExchangeEnhancedAuthenticationResult +{ + public string ReasonString { get; set; } + + public List UserProperties { get; set; } + + public byte[] AuthenticationData { get; set; } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResultFactory.cs b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResultFactory.cs new file mode 100644 index 000000000..7d8bddd21 --- /dev/null +++ b/Source/MQTTnet.Server/EnhancedAuthentication/ExchangeEnhancedAuthenticationResultFactory.cs @@ -0,0 +1,34 @@ +using MQTTnet.Packets; + +namespace MQTTnet.Server.EnhancedAuthentication; + +public static class ExchangeEnhancedAuthenticationResultFactory +{ + public static ExchangeEnhancedAuthenticationResult Create(MqttAuthPacket authPacket) + { + ArgumentNullException.ThrowIfNull(authPacket); + + return new ExchangeEnhancedAuthenticationResult + { + AuthenticationData = authPacket.AuthenticationData, + + ReasonString = authPacket.ReasonString, + UserProperties = authPacket.UserProperties + }; + } + + public static ExchangeEnhancedAuthenticationResult Create(MqttDisconnectPacket disconnectPacket) + { + ArgumentNullException.ThrowIfNull(disconnectPacket); + + return new ExchangeEnhancedAuthenticationResult + { + AuthenticationData = null, + ReasonString = disconnectPacket.ReasonString, + UserProperties = disconnectPacket.UserProperties + + // SessionExpiryInterval makes no sense because the connection is not yet made! + // ServerReferences makes no sense when the client initiated a DISCONNECT! + }; + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs index 325973c7e..e4e1fecfa 100644 --- a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs @@ -2,193 +2,243 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text; using MQTTnet.Adapter; +using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; +using MQTTnet.Server.EnhancedAuthentication; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ValidatingConnectionEventArgs : EventArgs { - public sealed class ValidatingConnectionEventArgs : EventArgs + readonly MqttConnectPacket _connectPacket; + + public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems, CancellationToken cancellationToken) + { + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + CancellationToken = cancellationToken; + } + + /// + /// Gets or sets the assigned client identifier. + /// MQTTv5 only. + /// + public string AssignedClientIdentifier { get; set; } + + /// + /// Gets or sets the authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] AuthenticationData => _connectPacket.AuthenticationData; + + /// + /// Gets or sets the authentication method. + /// MQTT 5.0.0+ feature. + /// + public string AuthenticationMethod => _connectPacket.AuthenticationMethod; + + public CancellationToken CancellationToken { get; } + + /// + /// Gets the channel adapter. This can be a _MqttConnectionContext_ (used in ASP.NET), a _MqttChannelAdapter_ (used for + /// TCP or WebSockets) or a custom implementation. + /// + public IMqttChannelAdapter ChannelAdapter { get; } + + /// + /// Gets or sets a value indicating whether clean sessions are used or not. + /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a + /// persistent connection. + /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for + /// the client. + /// This mode is ideal when the client only publishes messages. + /// It can also connect as a durable client using a persistent connection. + /// In this mode, the broker will store subscription information, and undelivered messages for the client. + /// + public bool? CleanSession => _connectPacket.CleanSession; + + public X509Certificate2 ClientCertificate => ChannelAdapter.ClientCertificate; + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId => _connectPacket.ClientId; + + [Obsolete("Use RemoteEndPoint instead.")] + public string Endpoint => RemoteEndPoint?.ToString(); + + public bool IsSecureConnection => ChannelAdapter.IsSecureConnection; + + /// + /// Gets or sets the keep alive period. + /// The connection is normally left open by the client so that is can send and receive data at any time. + /// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and + /// expect to receive a PINGRESP from the broker. + /// This message exchange confirms that the connection is open and working. + /// This period is known as the keep alive period. + /// + public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; + + /// + /// A value of 0 indicates that the value is not used. + /// + public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; + + public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array); + + public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; + + public byte[] RawPassword => _connectPacket.Password; + + /// + /// Gets or sets the reason code. When a MQTTv3 client connects the enum value must be one which is + /// also supported in MQTTv3. Otherwise the connection attempt will fail because not all codes can be + /// converted properly. + /// MQTT 5.0.0+ feature. + /// + public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; + + public string ReasonString { get; set; } + + /// + /// Gets or sets the receive maximum. + /// This gives the maximum length of the received messages. + /// A value of 0 indicates that the value is not used. + /// + public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum; + + public EndPoint RemoteEndPoint => ChannelAdapter.RemoteEndPoint; + + /// + /// Gets the request problem information. + /// MQTT 5.0.0+ feature. + /// + public bool RequestProblemInformation => _connectPacket.RequestProblemInformation; + + /// + /// Gets the request response information. + /// MQTT 5.0.0+ feature. + /// + public bool RequestResponseInformation => _connectPacket.RequestResponseInformation; + + /// + /// Gets or sets the response authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] ResponseAuthenticationData { get; set; } + + /// + /// Gets or sets the response user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// MQTT 5.0.0+ feature. + /// + public List ResponseUserProperties { get; set; } + + /// + /// Gets or sets the server reference. This can be used together with i.e. "Server Moved" to send + /// a different server address to the client. + /// MQTT 5.0.0+ feature. + /// + public string ServerReference { get; set; } + + /// + /// Gets the session expiry interval. + /// The time after a session expires when it's not actively used. + /// A value of 0 means no expiation. + /// + public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval; + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets or sets the topic alias maximum. + /// This gives the maximum length of the topic alias. + /// A value of 0 indicates that the value is not used. + /// + public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum; + + public string UserName => _connectPacket.Username; + + /// + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties => _connectPacket.UserProperties; + + /// + /// Gets or sets the delay interval for the will message. + /// This is the time between the client disconnect and the time the will message will be sent. + /// A value of 0 indicates that the value is not used. + /// + public uint WillDelayInterval => _connectPacket.WillDelayInterval; + + public async Task ExchangeEnhancedAuthenticationAsync( + ExchangeEnhancedAuthenticationOptions options, + CancellationToken cancellationToken = default) { - readonly MqttConnectPacket _connectPacket; + ArgumentNullException.ThrowIfNull(options); + + var requestAuthPacket = new MqttAuthPacket + { + // From RFC: If the initial CONNECT packet included an Authentication Method property then all AUTH packets, + // and any successful CONNACK packet MUST include an Authentication Method Property with the same value as in the CONNECT packet [MQTT-4.12.0-5]. + AuthenticationMethod = AuthenticationMethod, + + // The reason code will stay at continue all the time when connecting. The server will respond with the + // CONNACK packet when authentication is done! + ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, + + AuthenticationData = options.AuthenticationData, + ReasonString = options.ReasonString, + UserProperties = options.UserProperties + }; + + await ChannelAdapter.SendPacketAsync(requestAuthPacket, cancellationToken).ConfigureAwait(false); + + var responsePacket = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + + if (responsePacket == null) + { + throw new MqttCommunicationException("The client closed the connection."); + } + + if (responsePacket is MqttAuthPacket responseAuthPacket) + { + if (!string.Equals(AuthenticationMethod, responseAuthPacket.AuthenticationMethod, StringComparison.Ordinal)) + { + throw new MqttProtocolViolationException("The authentication method cannot change while authenticating the client."); + } + + return ExchangeEnhancedAuthenticationResultFactory.Create(responseAuthPacket); + } - public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems) + if (responsePacket is MqttDisconnectPacket disconnectPacket) { - _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); - ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + return ExchangeEnhancedAuthenticationResultFactory.Create(disconnectPacket); } - /// - /// Gets or sets the assigned client identifier. - /// MQTTv5 only. - /// - public string AssignedClientIdentifier { get; set; } - - /// - /// Gets or sets the authentication data. - /// MQTT 5.0.0+ feature. - /// - public byte[] AuthenticationData => _connectPacket.AuthenticationData; - - /// - /// Gets or sets the authentication method. - /// MQTT 5.0.0+ feature. - /// - public string AuthenticationMethod => _connectPacket.AuthenticationMethod; - - /// - /// Gets the channel adapter. This can be a _MqttConnectionContext_ (used in ASP.NET), a _MqttChannelAdapter_ (used for - /// TCP or WebSockets) or a custom implementation. - /// - public IMqttChannelAdapter ChannelAdapter { get; } - - /// - /// Gets or sets a value indicating whether clean sessions are used or not. - /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a - /// persistent connection. - /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for - /// the client. - /// This mode is ideal when the client only publishes messages. - /// It can also connect as a durable client using a persistent connection. - /// In this mode, the broker will store subscription information, and undelivered messages for the client. - /// - public bool? CleanSession => _connectPacket.CleanSession; - - public X509Certificate2 ClientCertificate => ChannelAdapter.ClientCertificate; - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId => _connectPacket.ClientId; - - public EndPoint RemoteEndPoint => ChannelAdapter.RemoteEndPoint; - - [Obsolete("Use RemoteEndPoint instead.")] - public string Endpoint => RemoteEndPoint?.ToString(); - - public bool IsSecureConnection => ChannelAdapter.IsSecureConnection; - - /// - /// Gets or sets the keep alive period. - /// The connection is normally left open by the client so that is can send and receive data at any time. - /// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and - /// expect to receive a PINGRESP from the broker. - /// This message exchange confirms that the connection is open and working. - /// This period is known as the keep alive period. - /// - public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; - - /// - /// A value of 0 indicates that the value is not used. - /// - public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; - - public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array); - - public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; - - public byte[] RawPassword => _connectPacket.Password; - - /// - /// Gets or sets the reason code. When a MQTTv3 client connects the enum value must be one which is - /// also supported in MQTTv3. Otherwise the connection attempt will fail because not all codes can be - /// converted properly. - /// MQTT 5.0.0+ feature. - /// - public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; - - public string ReasonString { get; set; } - - /// - /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. - /// A value of 0 indicates that the value is not used. - /// - public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum; - - /// - /// Gets the request problem information. - /// MQTT 5.0.0+ feature. - /// - public bool RequestProblemInformation => _connectPacket.RequestProblemInformation; - - /// - /// Gets the request response information. - /// MQTT 5.0.0+ feature. - /// - public bool RequestResponseInformation => _connectPacket.RequestResponseInformation; - - /// - /// Gets or sets the response authentication data. - /// MQTT 5.0.0+ feature. - /// - public byte[] ResponseAuthenticationData { get; set; } - - /// - /// Gets or sets the response user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// MQTT 5.0.0+ feature. - /// - public List ResponseUserProperties { get; set; } - - /// - /// Gets or sets the server reference. This can be used together with i.e. "Server Moved" to send - /// a different server address to the client. - /// MQTT 5.0.0+ feature. - /// - public string ServerReference { get; set; } - - /// - /// Gets the session expiry interval. - /// The time after a session expires when it's not actively used. - /// A value of 0 means no expiation. - /// - public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval; - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets or sets the topic alias maximum. - /// This gives the maximum length of the topic alias. - /// A value of 0 indicates that the value is not used. - /// - public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum; - - public string UserName => _connectPacket.Username; - - /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties => _connectPacket.UserProperties; - - /// - /// Gets or sets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// A value of 0 indicates that the value is not used. - /// - public uint WillDelayInterval => _connectPacket.WillDelayInterval; + throw new MqttProtocolViolationException("Received other packet than AUTH while authenticating."); } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs index 43a71abe3..2356b554b 100644 --- a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs @@ -364,7 +364,7 @@ public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter return; } - var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); + var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter, cancellationToken).ConfigureAwait(false); var connAckPacket = MqttConnAckPacketFactory.Create(validatingConnectionEventArgs); if (validatingConnectionEventArgs.ReasonCode != MqttConnectReasonCode.Success) @@ -740,11 +740,11 @@ static bool ShouldPersistSession(MqttConnectedClient connectedClient) } } - async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter) + async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) { // TODO: Load session items from persisted sessions in the future. var sessionItems = new ConcurrentDictionary(); - var eventArgs = new ValidatingConnectionEventArgs(connectPacket, channelAdapter, sessionItems); + var eventArgs = new ValidatingConnectionEventArgs(connectPacket, channelAdapter, sessionItems, cancellationToken); await _eventContainer.ValidatingConnectionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); // Check the client ID and set a random one if supported. diff --git a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs index 854696c73..f4bd1965e 100644 --- a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.ObjectModel; using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; diff --git a/Source/MQTTnet.Server/MqttServerFactory.cs b/Source/MQTTnet.Server/MqttServerFactory.cs index 0bb030f8b..36e640599 100644 --- a/Source/MQTTnet.Server/MqttServerFactory.cs +++ b/Source/MQTTnet.Server/MqttServerFactory.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using MQTTnet.Diagnostics.Logger; +using MQTTnet.Server.EnhancedAuthentication; using MQTTnet.Server.Internal.Adapter; namespace MQTTnet.Server; @@ -32,6 +33,11 @@ public MqttApplicationMessageBuilder CreateApplicationMessageBuilder() return new MqttApplicationMessageBuilder(); } + public ExchangeEnhancedAuthenticationOptionsFactory CreateExchangeExtendedAuthenticationOptionsBuilder() + { + return new ExchangeEnhancedAuthenticationOptionsFactory(); + } + public MqttServer CreateMqttServer(MqttServerOptions options) { return CreateMqttServer(options, DefaultLogger); diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs index 75b00d39d..ef8d75699 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -3,16 +3,16 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; -using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; +using MQTTnet.Server.EnhancedAuthentication; namespace MQTTnet.Tests.Clients.MqttClient { @@ -24,10 +24,8 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass public async Task Connect_To_Invalid_Server_Port_Not_Opened() { var client = new MqttClientFactory().CreateMqttClient(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); - } + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); } [TestMethod] @@ -35,10 +33,8 @@ public async Task Connect_To_Invalid_Server_Port_Not_Opened() public async Task Connect_To_Invalid_Server_Wrong_IP() { var client = new MqttClientFactory().CreateMqttClient(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); - } + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); } [TestMethod] @@ -53,179 +49,229 @@ public async Task Connect_To_Invalid_Server_Wrong_Protocol() public async Task ConnectTimeout_Throws_Exception() { var factory = new MqttClientFactory(); - using (var client = factory.CreateMqttClient()) + using var client = factory.CreateMqttClient(); + var disconnectHandlerCalled = false; + try { - var disconnectHandlerCalled = false; - try + client.DisconnectedAsync += _ => { - client.DisconnectedAsync += args => - { - disconnectHandlerCalled = true; - return CompletedTask.Instance; - }; - - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); + disconnectHandlerCalled = true; + return CompletedTask.Instance; + }; - Assert.Fail("Must fail!"); - } - catch (Exception exception) - { - Assert.IsNotNull(exception); - Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); - } + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - await LongTestDelay(); // disconnected handler is called async - Assert.IsTrue(disconnectHandlerCalled); + Assert.Fail("Must fail!"); } + catch (Exception exception) + { + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); + } + + await LongTestDelay(); // disconnected handler is called async + Assert.IsTrue(disconnectHandlerCalled); } [TestMethod] public async Task Disconnect_Clean() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; - var client = await testEnvironment.ConnectClient(); + var client = await testEnvironment.ConnectClient(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); - await LongTestDelay(); + await LongTestDelay(); - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); - } + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); } [TestMethod] public async Task Disconnect_Clean_With_Custom_Reason() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; - var client = await testEnvironment.ConnectClient(); + var client = await testEnvironment.ConnectClient(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); - await LongTestDelay(); + await LongTestDelay(); - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); - } + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); } [TestMethod] public async Task Disconnect_Clean_With_User_Properties() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; + + var client = await testEnvironment.ConnectClient(); + + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); + + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); + + await LongTestDelay(); + + Assert.IsNotNull(eventArgs); + Assert.IsNotNull(eventArgs.UserProperties); + Assert.AreEqual(1, eventArgs.UserProperties.Count); + Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); + Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + } + + class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler + { + public async Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs) { - var server = await testEnvironment.StartServer(); + if (eventArgs.AuthenticationMethod != "GS2-KRB5") + { + throw new InvalidOperationException("Wrong authentication method"); + } - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => + var sendOptions = new SendMqttEnhancedAuthenticationDataOptions { - eventArgs = args; - return CompletedTask.Instance; + Data = "initial context token"u8.ToArray() }; - var client = await testEnvironment.ConnectClient(); + await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); + var response = await eventArgs.ReceiveAsync(eventArgs.CancellationToken); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + Assert.AreEqual(Encoding.UTF8.GetString(response.AuthenticationData), "reply context token"); - await LongTestDelay(); + // No further data is required, but we have to fulfil the exchange. + sendOptions = new SendMqttEnhancedAuthenticationDataOptions + { + Data = [] + }; - Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs.UserProperties); - Assert.AreEqual(1, eventArgs.UserProperties.Count); - Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); - Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + await eventArgs.SendAsync(sendOptions, eventArgs.CancellationToken); } } [TestMethod] - public async Task No_Unobserved_Exception() + public async Task Use_Enhanced_Authentication() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += async args => { - testEnvironment.IgnoreClientLogErrors = true; + if (args.AuthenticationMethod == "GS2-KRB5") + { + var result = await args.ExchangeEnhancedAuthenticationAsync(new ExchangeEnhancedAuthenticationOptions(), args.CancellationToken); - var client = testEnvironment.CreateClient(); - var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); + Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), "initial context token"); - try - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5))) - { - await client.ConnectAsync(options, timeout.Token); - } + var authOptions = testEnvironment.ServerFactory.CreateExchangeExtendedAuthenticationOptionsBuilder().WithAuthenticationData("reply context token").Build(); + + result = await args.ExchangeEnhancedAuthenticationAsync(authOptions, args.CancellationToken); + + Assert.AreEqual(Encoding.UTF8.GetString(result.AuthenticationData), ""); + + args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray(); } - catch (OperationCanceledException) + else { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; } + }; + + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler(); - client.Dispose(); + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithEnhancedAuthentication("GS2-KRB5").WithEnhancedAuthenticationHandler(kerberosAuthenticationHandler); + var client = await testEnvironment.ConnectClient(clientOptions); - // These delays and GC calls are required in order to make calling the finalizer reproducible. - GC.Collect(); - GC.WaitForPendingFinalizers(); - await LongTestDelay(); - await LongTestDelay(); - await LongTestDelay(); + Assert.IsTrue(client.IsConnected); + } + + [TestMethod] + public async Task No_Unobserved_Exception() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var client = testEnvironment.CreateClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); + + try + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5)); + await client.ConnectAsync(options, timeout.Token); } + catch (OperationCanceledException) + { + } + + client.Dispose(); + + // These delays and GC calls are required in order to make calling the finalizer reproducible. + GC.Collect(); + GC.WaitForPendingFinalizers(); + await LongTestDelay(); + await LongTestDelay(); + await LongTestDelay(); } [TestMethod] public async Task Return_Non_Success() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - server.ValidatingConnectionAsync += args => - { - args.ResponseUserProperties = new List - { - new MqttUserProperty("Property", "Value") - }; + server.ValidatingConnectionAsync += args => + { + args.ResponseUserProperties = [new("Property", "Value")]; - args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; + args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; - return CompletedTask.Instance; - }; + return CompletedTask.Instance; + }; - var client = testEnvironment.CreateClient(); + var client = testEnvironment.CreateClient(); - var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); + var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); - Assert.IsNotNull(response); - Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); - Assert.AreEqual(response.UserProperties[0].Name, "Property"); - Assert.AreEqual(response.UserProperties[0].Value, "Value"); - } + Assert.IsNotNull(response); + Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); + Assert.AreEqual(response.UserProperties[0].Name, "Property"); + Assert.AreEqual(response.UserProperties[0].Value, "Value"); } [TestMethod] @@ -234,10 +280,8 @@ public async Task Throw_Proper_Exception_When_Not_Connected() try { var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); - } + using var mqttClient = mqttFactory.CreateMqttClient(); + await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); } catch (MqttClientNotConnectedException exception) { diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index c89d8057b..70390d884 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs b/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs deleted file mode 100644 index 1f93a8075..000000000 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet; - -public class MqttExtendedAuthenticationExchangeContext -{ - public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client) - { - ArgumentNullException.ThrowIfNull(authPacket); - - ReasonCode = authPacket.ReasonCode; - ReasonString = authPacket.ReasonString; - AuthenticationMethod = authPacket.AuthenticationMethod; - AuthenticationData = authPacket.AuthenticationData; - UserProperties = authPacket.UserProperties; - - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - /// - /// Gets the authentication data. - /// Hint: MQTT 5 feature only. - /// - public byte[] AuthenticationData { get; } - - /// - /// Gets the authentication method. - /// Hint: MQTT 5 feature only. - /// - public string AuthenticationMethod { get; } - - public MqttClient Client { get; } - - /// - /// Gets the reason code. - /// Hint: MQTT 5 feature only. - /// - public MqttAuthenticateReasonCode ReasonCode { get; } - - /// - /// Gets the reason string. - /// Hint: MQTT 5 feature only. - /// - public string ReasonString { get; } - - /// - /// Gets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. - /// - public List UserProperties { get; } -} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs index a471a6a7b..d183e1f4b 100644 --- a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs +++ b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public struct ReadFixedHeaderResult { - public struct ReadFixedHeaderResult + public static ReadFixedHeaderResult Canceled { get; } = new() + { + IsCanceled = true + }; + + public static ReadFixedHeaderResult ConnectionClosed { get; } = new() { - public static ReadFixedHeaderResult Canceled { get; } = new ReadFixedHeaderResult - { - IsCanceled = true - }; - - public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult - { - IsConnectionClosed = true - }; - - public bool IsCanceled { get; set; } - - public bool IsConnectionClosed { get; set; } + IsConnectionClosed = true + }; + + public bool IsCanceled { get; set; } + + public bool IsConnectionClosed { get; init; } - public MqttFixedHeader FixedHeader { get; set; } - } -} + public MqttFixedHeader FixedHeader { get; init; } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs index e27f129fc..7f6a11f1a 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs @@ -13,7 +13,7 @@ namespace MQTTnet.Formatter.V5 { public sealed class MqttV5PacketDecoder { - readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + readonly MqttBufferReader _bufferReader = new(); public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) { diff --git a/Source/MQTTnet/IMqttClient.cs b/Source/MQTTnet/IMqttClient.cs index f854c8ae3..445fd4e10 100644 --- a/Source/MQTTnet/IMqttClient.cs +++ b/Source/MQTTnet/IMqttClient.cs @@ -29,7 +29,7 @@ public interface IMqttClient : IDisposable Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default); - Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default); + Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default); Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default); diff --git a/Source/MQTTnet/MqttClient.cs b/Source/MQTTnet/MqttClient.cs index 9d19ce574..bb3248ff9 100644 --- a/Source/MQTTnet/MqttClient.cs +++ b/Source/MQTTnet/MqttClient.cs @@ -288,7 +288,7 @@ public Task PublishAsync(MqttApplicationMessage applica } } - public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default) + public Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(data); @@ -437,27 +437,30 @@ async Task Authenticate(IMqttChannelAdapter channelAdap var connectPacket = MqttConnectPacketFactory.Create(options); await Send(connectPacket, cancellationToken).ConfigureAwait(false); - var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); - - switch (receivedPacket) + while (true) { - case MqttConnAckPacket connAckPacket: - { - result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); - break; - } - case MqttAuthPacket _: + cancellationToken.ThrowIfCancellationRequested(); + + var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); + + if (receivedPacket is MqttAuthPacket authPacket) { - throw new NotSupportedException("Extended authentication handler is not yet supported"); + await HandleEnhancedAuthentication(authPacket, cancellationToken); + continue; } - case null: + + if (receivedPacket is MqttConnAckPacket connAckPacket) { - throw new MqttCommunicationException("Connection closed."); + result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); + break; } - default: + + if (receivedPacket != null) { - throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket})."); + throw new MqttProtocolViolationException($"Received other packet than CONNACK or AUTH while connecting ({receivedPacket})."); } + + throw new MqttCommunicationException("Connection closed."); } } catch (Exception exception) @@ -470,6 +473,12 @@ async Task Authenticate(IMqttChannelAdapter channelAdap return result; } + async Task HandleEnhancedAuthentication(MqttAuthPacket authPacket, CancellationToken cancellationToken) + { + var eventArgs = new MqttEnhancedAuthenticationEventArgs(authPacket, _adapter, cancellationToken); + await Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); + } + void Cleanup() { try @@ -646,12 +655,26 @@ Task OnConnected(MqttClientConnectResult connectResult) return _events.ConnectedEvent.InvokeAsync(eventArgs); } - Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket) + Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket, CancellationToken cancellationToken) { - var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler; - return extendedAuthenticationExchangeHandler != null - ? extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)) - : CompletedTask.Instance; + if (Options.EnhancedAuthenticationHandler == null) + { + // From RFC: If the re-authentication fails, the Client or Server SHOULD send DISCONNECT with an appropriate Reason Code + // as described in section 4.13, and MUST close the Network Connection [MQTT-4.12.1-2]. + // + // Since we have no handler there is no chance to fulfil the re-authentication request. + _ = DisconnectAsync(new MqttClientDisconnectOptions + { + Reason = MqttClientDisconnectOptionsReason.ImplementationSpecificError, + ReasonString = "Unable to handle AUTH packet" + }, + cancellationToken); + + return CompletedTask.Instance; + } + + var eventArgs = new MqttEnhancedAuthenticationEventArgs(authPacket, _adapter, cancellationToken); + return Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); } Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) @@ -959,7 +982,7 @@ async Task TryProcessReceivedPacket(MqttPacket packet, CancellationToken cancell await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false); break; case MqttAuthPacket authPacket: - await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false); + await ProcessReceivedAuthPacket(authPacket, cancellationToken).ConfigureAwait(false); break; case MqttPingRespPacket _: _packetDispatcher.TryDispatch(packet); diff --git a/Source/MQTTnet/MqttClientExtensions.cs b/Source/MQTTnet/MqttClientExtensions.cs index f6d09dd08..58d9c9f02 100644 --- a/Source/MQTTnet/MqttClientExtensions.cs +++ b/Source/MQTTnet/MqttClientExtensions.cs @@ -99,11 +99,11 @@ public static Task ReconnectAsync(this IMqttClient client, CancellationToken can return client.ConnectAsync(client.Options, cancellationToken); } - public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data) + public static Task SendEnhancedAuthenticationExchangeDataAsync(this IMqttClient client, MqttEnhancedAuthenticationExchangeData data) { ArgumentNullException.ThrowIfNull(client); - return client.SendExtendedAuthenticationExchangeDataAsync(data, CancellationToken.None); + return client.SendEnhancedAuthenticationExchangeDataAsync(data, CancellationToken.None); } public static Task SubscribeAsync(this IMqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default) diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs b/Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs similarity index 64% rename from Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs rename to Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs index b9454421d..e441eb05a 100644 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs +++ b/Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs @@ -6,7 +6,7 @@ namespace MQTTnet; -public interface IMqttExtendedAuthenticationExchangeHandler +public interface IMqttEnhancedAuthenticationHandler { - Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context); + Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs); } \ No newline at end of file diff --git a/Source/MQTTnet/Options/MqttClientOptions.cs b/Source/MQTTnet/Options/MqttClientOptions.cs index d17dd0548..85fc58868 100644 --- a/Source/MQTTnet/Options/MqttClientOptions.cs +++ b/Source/MQTTnet/Options/MqttClientOptions.cs @@ -13,7 +13,7 @@ namespace MQTTnet; public sealed class MqttClientOptions { /// - /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// Usually the MQTT packets can be sent partially. This is done by using multiple TCP packets /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and /// will close the connection when receiving such packets. If such a service is used this flag must /// be set to _false_. @@ -38,7 +38,7 @@ public sealed class MqttClientOptions /// Gets or sets a value indicating whether clean sessions are used or not. /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a /// persistent connection. - /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for + /// With a non-persistent connection the broker doesn't store any subscription information or undelivered messages for /// the client. /// This mode is ideal when the client only publishes messages. /// It can also connect as a durable client using a persistent connection. @@ -54,7 +54,12 @@ public sealed class MqttClientOptions public IMqttClientCredentialsProvider Credentials { get; set; } - public IMqttExtendedAuthenticationExchangeHandler ExtendedAuthenticationExchangeHandler { get; set; } + /// + /// Gets or sets the handler for AUTH packets. + /// This can happen when connecting or at any time while being already connected. + /// MQTT 5.0.0+ feature. + /// + public IMqttEnhancedAuthenticationHandler EnhancedAuthenticationHandler { get; set; } /// /// Gets or sets the keep alive period. @@ -80,7 +85,7 @@ public sealed class MqttClientOptions /// /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. + /// This gives the maximum length of the received messages. /// MQTT 5.0.0+ feature. /// public ushort ReceiveMaximum { get; set; } diff --git a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs index b1e14a797..a6e92c60d 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs @@ -2,14 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using MQTTnet.Formatter; -using MQTTnet.Packets; -using MQTTnet.Protocol; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet; @@ -98,13 +98,6 @@ public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily) return this; } - public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) - { - _options.AuthenticationMethod = method; - _options.AuthenticationData = data; - return this; - } - /// /// Clean session is used in MQTT versions below 5.0.0. It is the same as setting "CleanStart". /// @@ -138,27 +131,19 @@ public MqttClientOptionsBuilder WithConnectionUri(Uri uri) { case "tcp": case "mqtt": - WithTcpServer(uri.Host, port) - .WithAddressFamily(AddressFamily.Unspecified) - .WithProtocolType(ProtocolType.Tcp) - .WithTlsOptions(o => o.UseTls(false)); + WithTcpServer(uri.Host, port).WithAddressFamily(AddressFamily.Unspecified).WithProtocolType(ProtocolType.Tcp).WithTlsOptions(o => o.UseTls(false)); break; case "mqtts": - WithTcpServer(uri.Host, port) - .WithAddressFamily(AddressFamily.Unspecified) - .WithProtocolType(ProtocolType.Tcp) - .WithTlsOptions(o => o.UseTls(true)); + WithTcpServer(uri.Host, port).WithAddressFamily(AddressFamily.Unspecified).WithProtocolType(ProtocolType.Tcp).WithTlsOptions(o => o.UseTls()); break; case "ws": - WithWebSocketServer(o => o.WithUri(uri.ToString())) - .WithTlsOptions(o => o.UseTls(false)); + WithWebSocketServer(o => o.WithUri(uri.ToString())).WithTlsOptions(o => o.UseTls(false)); break; case "wss": - WithWebSocketServer(o => o.WithUri(uri.ToString())) - .WithTlsOptions(o => o.UseTls(true)); + WithWebSocketServer(o => o.WithUri(uri.ToString())).WithTlsOptions(o => o.UseTls()); break; // unix:///path/to/socket @@ -220,9 +205,16 @@ public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint) return this; } - public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) + public MqttClientOptionsBuilder WithEnhancedAuthentication(string method, byte[] data = null) + { + _options.AuthenticationMethod = method; + _options.AuthenticationData = data; + return this; + } + + public MqttClientOptionsBuilder WithEnhancedAuthenticationHandler(IMqttEnhancedAuthenticationHandler handler) { - _options.ExtendedAuthenticationExchangeHandler = handler; + _options.EnhancedAuthenticationHandler = handler; return this; } diff --git a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs index 19b2c91c3..3da66175f 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs @@ -105,6 +105,11 @@ public static void ThrowIfNotSupported(MqttClientOptions options) { Throw(nameof(options.WillUserProperties)); } + + if (options.EnhancedAuthenticationHandler != null) + { + Throw(nameof(options.EnhancedAuthenticationHandler)); + } } static void Throw(string featureName) diff --git a/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs new file mode 100644 index 000000000..0b9ac404f --- /dev/null +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet; + +public class MqttEnhancedAuthenticationEventArgs : EventArgs +{ + readonly IMqttChannelAdapter _channelAdapter; + readonly MqttAuthPacket _initialAuthPacket; + + public MqttEnhancedAuthenticationEventArgs(MqttAuthPacket initialAuthPacket, IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) + { + _initialAuthPacket = initialAuthPacket ?? throw new ArgumentNullException(nameof(initialAuthPacket)); + _channelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); + + CancellationToken = cancellationToken; + } + + /// + /// Gets the authentication data. + /// Hint: MQTT 5 feature only. + /// + public byte[] AuthenticationData => _initialAuthPacket.AuthenticationData; + + /// + /// Gets the authentication method. + /// Hint: MQTT 5 feature only. + /// + public string AuthenticationMethod => _initialAuthPacket.AuthenticationMethod; + + public CancellationToken CancellationToken { get; } + + /// + /// Gets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttAuthenticateReasonCode ReasonCode => _initialAuthPacket.ReasonCode; + + /// + /// Gets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString => _initialAuthPacket.ReasonString; + + /// + /// Gets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. + /// + public List UserProperties => _initialAuthPacket.UserProperties; + + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + { + var receivedPacket = await _channelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + + if (receivedPacket is MqttAuthPacket authPacket) + { + return new ReceiveMqttEnhancedAuthenticationDataResult + { + AuthenticationData = authPacket.AuthenticationData, + AuthenticationMethod = authPacket.AuthenticationMethod, + ReasonString = authPacket.ReasonString, + ReasonCode = authPacket.ReasonCode, + UserProperties = authPacket.UserProperties + }; + } + + if (receivedPacket is MqttConnAckPacket) + { + throw new InvalidOperationException("The enhanced authentication handler must not wait for the CONNACK packet."); + } + + throw new MqttProtocolViolationException("Received other packet than AUTH while authenticating."); + } + + public Task SendAsync(SendMqttEnhancedAuthenticationDataOptions options, CancellationToken cancellationToken = default) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var authPacket = new MqttAuthPacket + { + ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, + AuthenticationMethod = AuthenticationMethod, + AuthenticationData = options.Data, + UserProperties = options.UserProperties, + ReasonString = options.ReasonString + }; + + return _channelAdapter.SendPacketAsync(authPacket, cancellationToken); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs similarity index 97% rename from Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs rename to Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs index 2c58774c2..0e7ceafb8 100644 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs @@ -8,7 +8,7 @@ namespace MQTTnet; -public class MqttExtendedAuthenticationExchangeData +public class MqttEnhancedAuthenticationExchangeData { /// /// Gets or sets the authentication data. diff --git a/Source/MQTTnet/Options/ReceiveMqttEnhancedAuthenticationDataResult.cs b/Source/MQTTnet/Options/ReceiveMqttEnhancedAuthenticationDataResult.cs new file mode 100644 index 000000000..f2dc8cd20 --- /dev/null +++ b/Source/MQTTnet/Options/ReceiveMqttEnhancedAuthenticationDataResult.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet; + +public sealed class ReceiveMqttEnhancedAuthenticationDataResult +{ + public byte[] AuthenticationData { get; init; } + + public string AuthenticationMethod { get; init; } + + public MqttAuthenticateReasonCode ReasonCode { get; init; } + + public string ReasonString { get; init; } + + public List UserProperties { get; init; } +} \ No newline at end of file diff --git a/Source/MQTTnet/Options/SendMqttEnhancedAuthenticationDataOptions.cs b/Source/MQTTnet/Options/SendMqttEnhancedAuthenticationDataOptions.cs new file mode 100644 index 000000000..282b189d5 --- /dev/null +++ b/Source/MQTTnet/Options/SendMqttEnhancedAuthenticationDataOptions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet; + +public sealed class SendMqttEnhancedAuthenticationDataOptions +{ + public byte[] Data { get; init; } + + public string ReasonString { get; init; } + + public List UserProperties { get; init; } +} \ No newline at end of file diff --git a/Source/ReleaseNotes.md b/Source/ReleaseNotes.md index 402444509..e20cbffd6 100644 --- a/Source/ReleaseNotes.md +++ b/Source/ReleaseNotes.md @@ -8,8 +8,12 @@ * Namespace changes **(BREAKING CHANGE)** * Removal of Managed Client **(BREAKING CHANGE)** * Client: MQTT 5.0.0 is now the default version when connecting with a server **(BREAKING CHANGE)** +* Client: Fixed enhanced authentication. * Client: Exposed WebSocket compression options in MQTT client options (thanks to @victornor, #2127) * Server: Set default for "MaxPendingMessagesPerClient" to 1000 **(BREAKING CHANGE)** * Server: Set SSL version to "None" which will let the OS choose the version **(BREAKING CHANGE)** +* Server: Fixed enhanced authentication. +* Server: Set default for "MaxPendingMessagesPerClient" to 1000 **(BREAKING CHANGE)** +* Server: Set SSL version to "None" which will let the OS choose the version **(BREAKING CHANGE)** * Server: Added API for getting a single session (thanks to @AntonSmolkov, #2131) * Server: Fixed "TryPrivate" (Mosquitto feature) handling (thanks to @victornor, #2125) **(BREAKING CHANGE)**