From 7784d584bb8af08b1f7217fa6d7c572ac1918338 Mon Sep 17 00:00:00 2001
From: christian <6939810+chkr1011@users.noreply.github.com>
Date: Sun, 27 Oct 2024 18:54:00 +0100
Subject: [PATCH] Implement enhanced authentication for client and server.
---
.../Events/ValidatingConnectionEventArgs.cs | 372 ++++++++++--------
.../Internal/MqttClientSessionsManager.cs | 6 +-
.../Internal/MqttRetainedMessagesManager.cs | 1 +
.../MqttClient/MqttClient_Connection_Tests.cs | 276 +++++++------
Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 6 +-
...ttExtendedAuthenticationExchangeContext.cs | 63 ---
.../Formatter/ReadFixedHeaderResult.cs | 35 +-
.../Formatter/V5/MqttV5PacketDecoder.cs | 2 +-
Source/MQTTnet/IMqttClient.cs | 2 +-
Source/MQTTnet/MqttClient.cs | 60 ++-
Source/MQTTnet/MqttClientExtensions.cs | 4 +-
.../IMqttEnhancedAuthenticationHandler.cs} | 4 +-
Source/MQTTnet/Options/MqttClientOptions.cs | 13 +-
.../Options/MqttClientOptionsBuilder.cs | 6 +-
.../Options/MqttClientOptionsValidator.cs | 5 +
.../MqttEnhancedAuthenticationEventArgs.cs | 96 +++++
...MqttEnhancedAuthenticationExchangeData.cs} | 2 +-
17 files changed, 544 insertions(+), 409 deletions(-)
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%)
diff --git a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs
index cf44bdf52..6c8002f5d 100644
--- a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs
+++ b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs
@@ -2,189 +2,223 @@
// 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.Security.Cryptography.X509Certificates;
using System.Text;
using MQTTnet.Adapter;
+using MQTTnet.Exceptions;
using MQTTnet.Formatter;
using MQTTnet.Internal;
using MQTTnet.Packets;
using MQTTnet.Protocol;
-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;
+
+ public string Endpoint => ChannelAdapter.Endpoint;
+
+ 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;
+
+ public async Task ExchangeEnhancedAuthenticationAsync(byte[] authenticationData, CancellationToken cancellationToken = default)
{
- readonly MqttConnectPacket _connectPacket;
+ 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,
+ AuthenticationData = authenticationData,
+
+ // 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
+ };
+
+ 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.");
+ }
- public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems)
+ if (responsePacket is MqttAuthPacket responseAuthPacket)
{
- _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
- ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter));
- SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
+ // TODO: Wrap!
+ return responseAuthPacket;
}
- ///
- /// 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 string Endpoint => ChannelAdapter.Endpoint;
-
- 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;
+ // TODO: Support DISCONNECT-
+ 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 6c0cf68ec..66d8f78b6 100644
--- a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs
+++ b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs
@@ -348,7 +348,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)
@@ -710,11 +710,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