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
This commit is contained in:
Christian
2024-12-25 14:17:20 +01:00
committed by GitHub
parent f13b3eb6bb
commit 9dbe1e4b12
26 changed files with 1263 additions and 858 deletions
@@ -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));
}
/// <summary>
/// Gets the authentication data.
/// Hint: MQTT 5 feature only.
/// </summary>
public byte[] AuthenticationData { get; }
/// <summary>
/// Gets the authentication method.
/// Hint: MQTT 5 feature only.
/// </summary>
public string AuthenticationMethod { get; }
public MqttClient Client { get; }
/// <summary>
/// Gets the reason code.
/// Hint: MQTT 5 feature only.
/// </summary>
public MqttAuthenticateReasonCode ReasonCode { get; }
/// <summary>
/// Gets the reason string.
/// Hint: MQTT 5 feature only.
/// </summary>
public string ReasonString { get; }
/// <summary>
/// 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.
/// </summary>
public List<MqttUserProperty> UserProperties { get; }
}
@@ -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
{
public struct ReadFixedHeaderResult
{
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; }
namespace MQTTnet.Formatter;
public MqttFixedHeader FixedHeader { get; set; }
}
}
public struct ReadFixedHeaderResult
{
public static ReadFixedHeaderResult Canceled { get; } = new()
{
IsCanceled = true
};
public static ReadFixedHeaderResult ConnectionClosed { get; } = new()
{
IsConnectionClosed = true
};
public bool IsCanceled { get; set; }
public bool IsConnectionClosed { get; init; }
public MqttFixedHeader FixedHeader { get; init; }
}
@@ -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)
{
+1 -1
View File
@@ -29,7 +29,7 @@ public interface IMqttClient : IDisposable
Task<MqttClientPublishResult> PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default);
Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default);
Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default);
Task<MqttClientSubscribeResult> SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default);
+44 -21
View File
@@ -288,7 +288,7 @@ public sealed class MqttClient : Disposable, IMqttClient
}
}
public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default)
public Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(data);
@@ -437,27 +437,30 @@ public sealed class MqttClient : Disposable, IMqttClient
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:
cancellationToken.ThrowIfCancellationRequested();
var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false);
if (receivedPacket is MqttAuthPacket authPacket)
{
await HandleEnhancedAuthentication(authPacket, cancellationToken);
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)
@@ -470,6 +473,12 @@ public sealed class MqttClient : Disposable, IMqttClient
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 @@ public sealed class MqttClient : Disposable, IMqttClient
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 @@ public sealed class MqttClient : Disposable, IMqttClient
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);
+2 -2
View File
@@ -99,11 +99,11 @@ public static class MqttClientExtensions
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<MqttClientSubscribeResult> SubscribeAsync(this IMqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default)
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace MQTTnet;
public interface IMqttExtendedAuthenticationExchangeHandler
public interface IMqttEnhancedAuthenticationHandler
{
Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context);
Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs);
}
+9 -4
View File
@@ -13,7 +13,7 @@ namespace MQTTnet;
public sealed class MqttClientOptions
{
/// <summary>
/// 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; }
/// <summary>
/// Gets or sets the handler for AUTH packets.
/// This can happen when connecting or at any time while being already connected.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public IMqttEnhancedAuthenticationHandler EnhancedAuthenticationHandler { get; set; }
/// <summary>
/// Gets or sets the keep alive period.
@@ -80,7 +85,7 @@ public sealed class MqttClientOptions
/// <summary>
/// Gets or sets the receive maximum.
/// This gives the maximum length of the receive messages.
/// This gives the maximum length of the received messages.
/// <remarks>MQTT 5.0.0+ feature.</remarks>
/// </summary>
public ushort ReceiveMaximum { get; set; }
@@ -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 sealed class MqttClientOptionsBuilder
return this;
}
public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data)
{
_options.AuthenticationMethod = method;
_options.AuthenticationData = data;
return this;
}
/// <summary>
/// Clean session is used in MQTT versions below 5.0.0. It is the same as setting "CleanStart".
/// </summary>
@@ -138,27 +131,19 @@ public sealed class MqttClientOptionsBuilder
{
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 sealed class MqttClientOptionsBuilder
return this;
}
public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler)
public MqttClientOptionsBuilder WithEnhancedAuthentication(string method, byte[] data = null)
{
_options.ExtendedAuthenticationExchangeHandler = handler;
_options.AuthenticationMethod = method;
_options.AuthenticationData = data;
return this;
}
public MqttClientOptionsBuilder WithEnhancedAuthenticationHandler(IMqttEnhancedAuthenticationHandler handler)
{
_options.EnhancedAuthenticationHandler = handler;
return this;
}
@@ -105,6 +105,11 @@ public static class MqttClientOptionsValidator
{
Throw(nameof(options.WillUserProperties));
}
if (options.EnhancedAuthenticationHandler != null)
{
Throw(nameof(options.EnhancedAuthenticationHandler));
}
}
static void Throw(string featureName)
@@ -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;
}
/// <summary>
/// Gets the authentication data.
/// Hint: MQTT 5 feature only.
/// </summary>
public byte[] AuthenticationData => _initialAuthPacket.AuthenticationData;
/// <summary>
/// Gets the authentication method.
/// Hint: MQTT 5 feature only.
/// </summary>
public string AuthenticationMethod => _initialAuthPacket.AuthenticationMethod;
public CancellationToken CancellationToken { get; }
/// <summary>
/// Gets the reason code.
/// Hint: MQTT 5 feature only.
/// </summary>
public MqttAuthenticateReasonCode ReasonCode => _initialAuthPacket.ReasonCode;
/// <summary>
/// Gets the reason string.
/// Hint: MQTT 5 feature only.
/// </summary>
public string ReasonString => _initialAuthPacket.ReasonString;
/// <summary>
/// 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.
/// </summary>
public List<MqttUserProperty> UserProperties => _initialAuthPacket.UserProperties;
public async Task<ReceiveMqttEnhancedAuthenticationDataResult> 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);
}
}
@@ -8,7 +8,7 @@ using MQTTnet.Protocol;
namespace MQTTnet;
public class MqttExtendedAuthenticationExchangeData
public class MqttEnhancedAuthenticationExchangeData
{
/// <summary>
/// Gets or sets the authentication data.
@@ -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<MqttUserProperty> UserProperties { get; init; }
}
@@ -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<MqttUserProperty> UserProperties { get; init; }
}