From 4fb3604ad6e91ac77b11f9de2bd395f3f6ffea7c Mon Sep 17 00:00:00 2001 From: BillArmstrong Date: Sat, 23 Aug 2025 08:16:01 -0400 Subject: [PATCH] Backport of: 2132 Fix support for enhanced authentication (#2171) --- .github/workflows/ReleaseNotes.md | 4 +- Samples/Client/Client_Connection_Samples.cs | 102 ++++++++++++++++++ .../MqttClient/MqttClient_Connection_Tests.cs | 86 +++++++++++++++ ...ttExtendedAuthenticationExchangeContext.cs | 9 +- Source/MQTTnet/Client/MqttClient.cs | 81 ++++++++++---- .../Options/MqttClientOptionsValidator.cs | 7 +- Source/MQTTnet/MqttFactory.cs | 5 + .../Events/ValidatingConnectionEventArgs.cs | 52 +++++++++ ...ttExtendedAuthenticationExchangeOptions.cs | 18 ++++ ...dedAuthenticationExchangeOptionsFactory.cs | 73 +++++++++++++ ...qttExtendedAuthenticationExchangeResult.cs | 18 ++++ ...ndedAuthenticationExchangeResultFactory.cs | 46 ++++++++ 12 files changed, 480 insertions(+), 21 deletions(-) create mode 100644 Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptions.cs create mode 100644 Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptionsFactory.cs create mode 100644 Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResult.cs create mode 100644 Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResultFactory.cs diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index 4a62f4b9..6bd17943 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1 +1,3 @@ -* Client: Fixed wrong timeout for keep alive check (thanks to @Erw1nT, #2129) \ No newline at end of file +* Client: Fixed wrong timeout for keep alive check (thanks to @Erw1nT, #2129) +* Client: Fixed extended authentication (thanks to @chkr1011, #2132). +* Server: Fixed extended authentication (thanks to @chkr1011, #2132). \ No newline at end of file diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs index 65c3c652..b6a1a0ad 100644 --- a/Samples/Client/Client_Connection_Samples.cs +++ b/Samples/Client/Client_Connection_Samples.cs @@ -8,10 +8,13 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Text; using MQTTnet.Client; using MQTTnet.Extensions.WebSocket4Net; using MQTTnet.Formatter; +using MQTTnet.Protocol; using MQTTnet.Samples.Helpers; +using MQTTnet.Server; namespace MQTTnet.Samples.Client; @@ -100,6 +103,105 @@ public static class Client_Connection_Samples } } + public static async Task Connect_Using_Extended_Authentication() + { + /* + * This sample uses enhanced (aka extended) authentication (Kerberos) when creating the connection. + */ + + /* + * Server part... + */ + + var mqttFactory = new MqttFactory(); + + var serverOptions = mqttFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().Build(); + var server = mqttFactory.CreateMqttServer(serverOptions); + + server.ValidatingConnectionAsync += async args => + { + if (args.AuthenticationMethod == "GS2-KRB5") + { + var result = await args.SendExtendedAuthenticationExchangeDataAsync(new MqttExtendedAuthenticationExchangeOptions()); + + Console.WriteLine($"Received AUTH data from client: {Encoding.UTF8.GetString(result.AuthenticationData)}"); + + var authOptions = mqttFactory.CreateMqttExtendedAuthenticationExchangeOptionsBuilder().WithAuthenticationData("reply context token").Build(); + + result = await args.SendExtendedAuthenticationExchangeDataAsync(authOptions); + + 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! + } + else + { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; + } + }; + + await server.StartAsync(); + + /* + * Client part... + */ + + var mqttClientFactory = new MqttFactory(); + + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new SampleClientKerberosAuthenticationHandler(); + + var clientOptions = mqttClientFactory.CreateClientOptionsBuilder() + .WithTcpServer("localhost") + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithExtendedAuthenticationExchangeHandler(kerberosAuthenticationHandler) + .WithAuthentication("GS2-KRB5", Array.Empty()) + .Build(); + + var client = mqttClientFactory.CreateMqttClient(); + + var result = await client.ConnectAsync(clientOptions); + + Console.WriteLine($"Client connect result: {result.ResultCode}"); + } + + class SampleClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler + { + private readonly Queue clientTokens = new Queue(); + + public SampleClientKerberosAuthenticationHandler() + { + // The Kerberos sample from the MQTT RFC sends two client tokens after the initial empty token. + clientTokens.Enqueue(Encoding.UTF8.GetBytes("initial context token")); + clientTokens.Enqueue(Array.Empty()); + } + + public async Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context) + { + if (context.AuthenticationMethod != "GS2-KRB5") + { + throw new InvalidOperationException("Wrong authentication method"); + } + + Console.WriteLine($"Received AUTH data from server: {Encoding.UTF8.GetString(context.AuthenticationData)}"); + + if (!clientTokens.TryDequeue(out var clientToken)) + { + throw new InvalidOperationException("No more client tokens available"); + } + + var sendOptions = new MqttExtendedAuthenticationExchangeData + { + AuthenticationData = clientToken + }; + + await context.Client.SendExtendedAuthenticationExchangeDataAsync(sendOptions); + } + } + public static async Task Connect_Client_Using_MQTTv5() { /* diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs index cb7b2d23..ad8c1e7e 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -167,6 +168,91 @@ namespace MQTTnet.Tests.Clients.MqttClient } } + class TestClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler + { + public static IReadOnlyList ServerTokens { get; } = new List() + { + string.Empty, + "reply context token" + }; + + public static IReadOnlyList ClientTokens { get; } = new List() + { + string.Empty, + "initial context token", + string.Empty + }; + + int tokenCursor; + + public async Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context) + { + if (context.AuthenticationMethod != "GS2-KRB5") + { + throw new InvalidOperationException("Wrong authentication method"); + } + + Assert.IsTrue(tokenCursor < ServerTokens.Count, "No more server tokens were expected."); + var expectedToken = ServerTokens[tokenCursor++]; + Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(context.AuthenticationData)); + + var sendOptions = new MqttExtendedAuthenticationExchangeData + { + AuthenticationData = Encoding.UTF8.GetBytes(ClientTokens[tokenCursor]) + }; + + await context.Client.SendExtendedAuthenticationExchangeDataAsync(sendOptions, context.CancellationToken); + } + } + + [TestMethod] + public async Task Use_Extended_Authentication() + { + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler(); + + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += async args => + { + if (args.AuthenticationMethod == "GS2-KRB5") + { + var authenticationData = args.AuthenticationData; + string expectedToken; + int tokenCursor; + for (tokenCursor = 0; tokenCursor < TestClientKerberosAuthenticationHandler.ServerTokens.Count; tokenCursor++) + { + expectedToken = TestClientKerberosAuthenticationHandler.ClientTokens[tokenCursor]; + Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(authenticationData), "The received client token is not correct."); + + var serverToken = TestClientKerberosAuthenticationHandler.ServerTokens[tokenCursor]; + var authOptions = testEnvironment.Factory.CreateMqttExtendedAuthenticationExchangeOptionsBuilder().WithAuthenticationData(serverToken).Build(); + var result = await args.SendExtendedAuthenticationExchangeDataAsync(authOptions); + + authenticationData = result.AuthenticationData; + } + + expectedToken = TestClientKerberosAuthenticationHandler.ClientTokens[tokenCursor]; + Assert.AreEqual(expectedToken, Encoding.UTF8.GetString(authenticationData), "The received client token is not correct."); + args.ResponseAuthenticationData = Encoding.UTF8.GetBytes("outcome of authentication"); + args.ReasonCode = MqttConnectReasonCode.Success; + args.ReasonString = "Authentication successful"; + } + else + { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; + } + }; + + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithAuthentication("GS2-KRB5", new byte[0]).WithExtendedAuthenticationExchangeHandler(kerberosAuthenticationHandler); + var client = await testEnvironment.ConnectClient(clientOptions); + + Assert.IsTrue(client.IsConnected); + } + } + [TestMethod] public async Task No_Unobserved_Exception() { diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs index c406b5ec..5a3518f8 100644 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs +++ b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Threading; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -11,7 +12,7 @@ namespace MQTTnet.Client { public class MqttExtendedAuthenticationExchangeContext { - public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client) + public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client, CancellationToken cancellationToken) { if (authPacket == null) throw new ArgumentNullException(nameof(authPacket)); @@ -22,6 +23,7 @@ namespace MQTTnet.Client UserProperties = authPacket.UserProperties; Client = client ?? throw new ArgumentNullException(nameof(client)); + CancellationToken = cancellationToken; } /// @@ -58,5 +60,10 @@ namespace MQTTnet.Client public List UserProperties { get; } public MqttClient Client { get; } + + /// + /// The cancellation token passed to the operation being authenticated (e.g. ConnectAsync). + /// + public CancellationToken CancellationToken { get; } } } diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 8a79421c..e8f5bcdc 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -89,6 +89,8 @@ namespace MQTTnet.Client public bool IsConnected => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connected; + bool IsConnecting => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connecting; + public MqttClientOptions Options { get; private set; } public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken = default) @@ -304,7 +306,7 @@ namespace MQTTnet.Client } ThrowIfDisposed(); - ThrowIfNotConnected(); + ThrowIfNotConnectedOrConnecting(); var authPacket = new MqttAuthPacket { @@ -454,27 +456,30 @@ namespace MQTTnet.Client var connectPacket = MqttPacketFactories.Connect.Create(options); await Send(connectPacket, cancellationToken).ConfigureAwait(false); - var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); - - switch (receivedPacket) + for (; ; ) { - case MqttConnAckPacket connAckPacket: + cancellationToken.ThrowIfCancellationRequested(); + + var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); + + if (receivedPacket is MqttAuthPacket authPacket) + { + await ProcessReceivedAuthPacket(authPacket, cancellationToken).ConfigureAwait(false); + continue; + } + + if (receivedPacket is MqttConnAckPacket connAckPacket) { result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); break; } - case MqttAuthPacket _: + + if (receivedPacket != null) { - throw new NotSupportedException("Extended authentication handler is not yet supported"); - } - case null: - { - throw new MqttCommunicationException("Connection closed."); - } - default: - { - 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) @@ -678,10 +683,37 @@ namespace MQTTnet.Client 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 (extendedAuthenticationExchangeHandler == null) + { + // From RFC: + // The Client can close the connection at any point in this process. It MAY send a DISCONNECT packet before doing so. + // The Server can reject the authentication at any point in this process. It MAY send a CONNACK with a Reason Code of + // 0x80 or above as described in section 4.13, and MUST close the Network Connection [MQTT-4.12.0-4]. + // + // 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 initial authentication or re-authentication requests. + + if (IsConnecting) + { + throw new MqttCommunicationException("No handler was provided to handle the extended authentication exchange."); + } + + _ = DisconnectAsync(new MqttClientDisconnectOptions + { + Reason = MqttClientDisconnectOptionsReason.ImplementationSpecificError, + ReasonString = "Unable to handle AUTH packet" + }, + cancellationToken); + + return CompletedTask.Instance; + } + + return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this, cancellationToken)); } Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) @@ -935,6 +967,14 @@ namespace MQTTnet.Client } } + void ThrowIfNotConnectedOrConnecting() + { + if (!(IsConnected || IsConnecting)) + { + ThrowNotConnectedOrConnecting(); + } + } + static void ThrowIfOptionsInvalid(MqttClientOptions options) { if (options == null) @@ -958,6 +998,11 @@ namespace MQTTnet.Client throw new MqttClientNotConnectedException(); } + static void ThrowNotConnectedOrConnecting() + { + throw new MqttCommunicationException("The MQTT client is not connected or connecting."); + } + void TryInitiateDisconnect() { lock (_disconnectLock) @@ -992,7 +1037,7 @@ namespace MQTTnet.Client 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/Client/Options/MqttClientOptionsValidator.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsValidator.cs index 38ddbfb4..c6599311 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsValidator.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsValidator.cs @@ -68,8 +68,13 @@ namespace MQTTnet.Client Throw(nameof(options.AuthenticationMethod)); } + if (options.ExtendedAuthenticationExchangeHandler != null) + { + Throw(nameof(options.ExtendedAuthenticationExchangeHandler)); + } + // Will relevant properties. - + if (options.WillPayloadFormatIndicator != MqttPayloadFormatIndicator.Unspecified) { Throw(nameof(options.WillPayloadFormatIndicator)); diff --git a/Source/MQTTnet/MqttFactory.cs b/Source/MQTTnet/MqttFactory.cs index e3a0ea8b..a390ea50 100644 --- a/Source/MQTTnet/MqttFactory.cs +++ b/Source/MQTTnet/MqttFactory.cs @@ -135,6 +135,11 @@ namespace MQTTnet return new MqttClient(clientAdapterFactory, logger); } + public MqttExtendedAuthenticationExchangeOptionsFactory CreateMqttExtendedAuthenticationExchangeOptionsBuilder() + { + return new MqttExtendedAuthenticationExchangeOptionsFactory(); + } + public MqttServer CreateMqttServer(MqttServerOptions options) { return CreateMqttServer(options, DefaultLogger); diff --git a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs index 096b2da1..2fa1a8ca 100644 --- a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs @@ -7,11 +7,14 @@ using System.Collections; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading.Tasks; +using System.Threading; using MQTTnet.Adapter; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; +using MQTTnet.Exceptions; namespace MQTTnet.Server { @@ -189,5 +192,54 @@ namespace MQTTnet.Server /// A value of 0 indicates that the value is not used. /// public uint WillDelayInterval => _connectPacket.WillDelayInterval; + + + public async Task SendExtendedAuthenticationExchangeDataAsync( + MqttExtendedAuthenticationExchangeOptions options, + CancellationToken cancellationToken = default) + { + if (options == null) throw new ArgumentNullException(nameof(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 MqttExtendedAuthenticationExchangeResultFactory.Create(responseAuthPacket); + } + + if (responsePacket is MqttDisconnectPacket disconnectPacket) + { + return MqttExtendedAuthenticationExchangeResultFactory.Create(disconnectPacket); + } + + throw new MqttProtocolViolationException("Received other packet than AUTH while authenticating."); + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptions.cs b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptions.cs new file mode 100644 index 00000000..4a81fd95 --- /dev/null +++ b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptions.cs @@ -0,0 +1,18 @@ +// 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; +using System.Collections.Generic; + +namespace MQTTnet.Server +{ + public sealed class MqttExtendedAuthenticationExchangeOptions + { + public byte[] AuthenticationData { get; set; } + + public string ReasonString { get; set; } + + public List UserProperties { get; set; } + } +} diff --git a/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptionsFactory.cs b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptionsFactory.cs new file mode 100644 index 00000000..966abee9 --- /dev/null +++ b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeOptionsFactory.cs @@ -0,0 +1,73 @@ +// 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 System.Text; +using System; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class MqttExtendedAuthenticationExchangeOptionsFactory + { + readonly MqttExtendedAuthenticationExchangeOptions _options = new MqttExtendedAuthenticationExchangeOptions(); + + public MqttExtendedAuthenticationExchangeOptions Build() + { + return _options; + } + + public MqttExtendedAuthenticationExchangeOptionsFactory WithAuthenticationData(byte[] authenticationData) + { + _options.AuthenticationData = authenticationData; + + return this; + } + + public MqttExtendedAuthenticationExchangeOptionsFactory WithAuthenticationData(string authenticationData) + { + if (authenticationData == null) + { + _options.AuthenticationData = null; + } + else + { + _options.AuthenticationData = Encoding.UTF8.GetBytes(authenticationData); + } + + return this; + } + + public MqttExtendedAuthenticationExchangeOptionsFactory WithReasonString(string reasonString) + { + _options.ReasonString = reasonString; + + return this; + } + + public MqttExtendedAuthenticationExchangeOptionsFactory WithUserProperties(List userProperties) + { + _options.UserProperties = userProperties; + + return this; + } + + public MqttExtendedAuthenticationExchangeOptionsFactory 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; + } + } +} diff --git a/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResult.cs b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResult.cs new file mode 100644 index 00000000..604ac2a1 --- /dev/null +++ b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResult.cs @@ -0,0 +1,18 @@ +// 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; +using System.Collections.Generic; + +namespace MQTTnet.Server +{ + public sealed class MqttExtendedAuthenticationExchangeResult + { + public string ReasonString { get; set; } + + public List UserProperties { get; set; } + + public byte[] AuthenticationData { get; set; } + } +} diff --git a/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResultFactory.cs b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResultFactory.cs new file mode 100644 index 00000000..925f042b --- /dev/null +++ b/Source/MQTTnet/Server/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResultFactory.cs @@ -0,0 +1,46 @@ +// 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; +using System; + +namespace MQTTnet.Server +{ + public static class MqttExtendedAuthenticationExchangeResultFactory + { + public static MqttExtendedAuthenticationExchangeResult Create(MqttAuthPacket authPacket) + { + if (authPacket == null) + { + throw new ArgumentNullException(nameof(authPacket)); + } + + return new MqttExtendedAuthenticationExchangeResult + { + AuthenticationData = authPacket.AuthenticationData, + + ReasonString = authPacket.ReasonString, + UserProperties = authPacket.UserProperties + }; + } + + public static MqttExtendedAuthenticationExchangeResult Create(MqttDisconnectPacket disconnectPacket) + { + if (disconnectPacket == null) + { + throw new ArgumentNullException(nameof(disconnectPacket)); + } + + return new MqttExtendedAuthenticationExchangeResult + { + 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! + }; + } + } +}