Skip to content

Backport of: 2132 Fix support for enhanced authentication #2171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: release/4.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
* Client: Fixed wrong timeout for keep alive check (thanks to @Erw1nT, #2129)
* 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).
102 changes: 102 additions & 0 deletions Samples/Client/Client_Connection_Samples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -100,6 +103,105 @@ public static async Task Connect_Client_Timeout()
}
}

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<byte>())
.Build();

var client = mqttClientFactory.CreateMqttClient();

var result = await client.ConnectAsync(clientOptions);

Console.WriteLine($"Client connect result: {result.ResultCode}");
}

class SampleClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler
{
private readonly Queue<byte[]> clientTokens = new Queue<byte[]>();

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<byte>());
}

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()
{
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -167,6 +168,91 @@ public async Task Disconnect_Clean_With_User_Properties()
}
}

class TestClientKerberosAuthenticationHandler : IMqttExtendedAuthenticationExchangeHandler
{
public static IReadOnlyList<string> ServerTokens { get; } = new List<string>()
{
string.Empty,
"reply context token"
};

public static IReadOnlyList<string> ClientTokens { get; } = new List<string>()
{
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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

using System;
using System.Collections.Generic;
using System.Threading;
using MQTTnet.Packets;
using MQTTnet.Protocol;

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));

Expand All @@ -22,6 +23,7 @@ public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, Mqtt
UserProperties = authPacket.UserProperties;

Client = client ?? throw new ArgumentNullException(nameof(client));
CancellationToken = cancellationToken;
}

/// <summary>
Expand Down Expand Up @@ -58,5 +60,10 @@ public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, Mqtt
public List<MqttUserProperty> UserProperties { get; }

public MqttClient Client { get; }

/// <summary>
/// The cancellation token passed to the operation being authenticated (e.g. ConnectAsync).
/// </summary>
public CancellationToken CancellationToken { get; }
}
}
Loading