Skip to content
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

Add support for auth in URL #670

Merged
merged 14 commits into from
Nov 5, 2024
62 changes: 60 additions & 2 deletions src/NATS.Client.Core/NatsConnection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using NATS.Client.Core.Commands;
@@ -76,7 +75,7 @@ public NatsConnection()
public NatsConnection(NatsOpts opts)
{
_logger = opts.LoggerFactory.CreateLogger<NatsConnection>();
Opts = opts;
Opts = ReadUserInfoFromConnectionString(opts);
ConnectionState = NatsConnectionState.Closed;
_waitForOpenConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_disposedCancellationTokenSource = new CancellationTokenSource();
@@ -289,6 +288,65 @@ internal ValueTask UnsubscribeAsync(int sid)
return default;
}

private static NatsOpts ReadUserInfoFromConnectionString(NatsOpts opts)
{
var first = true;

var natsUris = opts.GetSeedUris(suppressRandomization: true);
var maskedUris = new List<string>(natsUris.Length);

foreach (var natsUri in natsUris)
{
var uriBuilder = new UriBuilder(natsUri.Uri);

if (uriBuilder.UserName is { Length: > 0 } username)
{
if (uriBuilder.Password is { Length: > 0 } password)
{
if (first)
{
first = false;

opts = opts with
{
AuthOpts = opts.AuthOpts with
{
Username = uriBuilder.UserName,
Password = uriBuilder.Password,
},
};
}

uriBuilder.Password = "***"; // to redact the password from logs
}
else
{
if (first)
{
first = false;

opts = opts with
{
AuthOpts = opts.AuthOpts with
{
Token = uriBuilder.UserName,
},
};
}

uriBuilder.UserName = "***"; // to redact the token from logs
}
}

maskedUris.Add(uriBuilder.ToString().TrimEnd('/'));
}

var combinedUri = string.Join(",", maskedUris);
opts = opts with { Url = combinedUri };

return opts;
}

private async ValueTask InitialConnectAsync()
{
Debug.Assert(ConnectionState == NatsConnectionState.Connecting, "Connection state");
4 changes: 2 additions & 2 deletions src/NATS.Client.Core/NatsOpts.cs
Original file line number Diff line number Diff line change
@@ -117,10 +117,10 @@ public sealed record NatsOpts
/// </remarks>
public BoundedChannelFullMode SubPendingChannelFullMode { get; init; } = BoundedChannelFullMode.DropNewest;

internal NatsUri[] GetSeedUris()
internal NatsUri[] GetSeedUris(bool suppressRandomization = false)
{
var urls = Url.Split(',');
return NoRandomize
return NoRandomize || suppressRandomization
? urls.Select(x => new NatsUri(x, true)).Distinct().ToArray()
: urls.Select(x => new NatsUri(x, true)).OrderBy(_ => Guid.NewGuid()).Distinct().ToArray();
}
32 changes: 26 additions & 6 deletions tests/NATS.Client.Core.Tests/ClusterTests.cs
Original file line number Diff line number Diff line change
@@ -2,23 +2,43 @@ namespace NATS.Client.Core.Tests;

public class ClusterTests(ITestOutputHelper output)
{
[Fact]
public async Task Seed_urls_on_retry()
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Seed_urls_on_retry(bool userAuthInUrl)
{
await using var cluster1 = new NatsCluster(
new NullOutputHelper(),
TransportType.Tcp,
(i, b) => b.WithServerName($"c1n{i}"));
(i, b) =>
{
b.WithServerName($"c1n{i}");
if (userAuthInUrl)
{
b.AddServerConfig("resources/configs/auth/password.conf");
b.WithClientUrlAuthentication("a", "b");
}
},
userAuthInUrl);

await using var cluster2 = new NatsCluster(
new NullOutputHelper(),
TransportType.Tcp,
(i, b) => b.WithServerName($"c2n{i}"));
(i, b) =>
{
b.WithServerName($"c2n{i}");
if (userAuthInUrl)
{
b.AddServerConfig("resources/configs/auth/password.conf");
b.WithClientUrlAuthentication("a", "b");
}
},
userAuthInUrl);

// Use the first node from each cluster as the seed
// so that we can confirm seeds are used on retry
var url1 = cluster1.Server1.ClientUrl;
var url2 = cluster2.Server1.ClientUrl;
var url1 = userAuthInUrl ? cluster1.Server1.ClientUrlWithAuth : cluster1.Server1.ClientUrl;
var url2 = userAuthInUrl ? cluster2.Server1.ClientUrlWithAuth : cluster2.Server1.ClientUrl;

await using var nats = new NatsConnection(new NatsOpts
{
44 changes: 36 additions & 8 deletions tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs
Original file line number Diff line number Diff line change
@@ -12,6 +12,15 @@
NatsOpts.Default with { AuthOpts = NatsAuthOpts.Default with { Token = "s3cr3t", }, }),
};

yield return new object[]
{
new Auth(
"TOKEN_IN_CONNECTIONSTRING",
"resources/configs/auth/token.conf",
NatsOpts.Default,
urlAuth: "s3cr3t"),
};

yield return new object[]
{
new Auth(
@@ -23,6 +32,15 @@
}),
};

yield return new object[]
{
new Auth(
"USER-PASSWORD_IN_CONNECTIONSTRING",
"resources/configs/auth/password.conf",
NatsOpts.Default,
urlAuth: "a:b"),
};

yield return new object[]
{
new Auth(
@@ -84,15 +102,22 @@
var name = auth.Name;
var serverConfig = auth.ServerConfig;
var clientOpts = auth.ClientOpts;
var useAuthInUrl = !string.IsNullOrEmpty(auth.UrlAuth);

_output.WriteLine($"AUTH TEST {name}");

var serverOpts = new NatsServerOptsBuilder()
var serverOptsBuilder = new NatsServerOptsBuilder()
.UseTransport(_transportType)
.AddServerConfig(serverConfig)
.Build();
.AddServerConfig(serverConfig);

if (useAuthInUrl)
{
serverOptsBuilder.WithClientUrlAuthentication(auth.UrlAuth);
}

await using var server = NatsServer.Start(_output, serverOpts, clientOpts);
var serverOpts = serverOptsBuilder.Build();

await using var server = NatsServer.Start(_output, serverOpts, clientOpts, useAuthInUrl);

var subject = Guid.NewGuid().ToString("N");

@@ -104,8 +129,8 @@
Assert.Contains("Authorization Violation", natsException.GetBaseException().Message);
}

await using var subConnection = server.CreateClientConnection(clientOpts);
await using var pubConnection = server.CreateClientConnection(clientOpts);
await using var subConnection = server.CreateClientConnection(clientOpts, useAuthInUrl: useAuthInUrl);
await using var pubConnection = server.CreateClientConnection(clientOpts, useAuthInUrl: useAuthInUrl);

var signalComplete1 = new WaitSignal();
var signalComplete2 = new WaitSignal();
@@ -141,7 +166,7 @@
await disconnectSignal2;

_output.WriteLine("START NEW SERVER");
await using var newServer = NatsServer.Start(_output, serverOpts, clientOpts);
await using var newServer = NatsServer.Start(_output, serverOpts, clientOpts, useAuthInUrl);
await subConnection.ConnectAsync(); // wait open again
await pubConnection.ConnectAsync(); // wait open again

@@ -162,11 +187,12 @@

public class Auth
{
public Auth(string name, string serverConfig, NatsOpts clientOpts)
public Auth(string name, string serverConfig, NatsOpts clientOpts, string urlAuth = null)

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (v2.9)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Windows (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (main)

Cannot convert null literal to non-nullable reference type.

Check warning on line 190 in tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs

GitHub Actions / Linux (main)

Cannot convert null literal to non-nullable reference type.
{
Name = name;
ServerConfig = serverConfig;
ClientOpts = clientOpts;
UrlAuth = urlAuth;
}

public string Name { get; }
@@ -175,6 +201,8 @@

public NatsOpts ClientOpts { get; }

public string UrlAuth { get; }

public override string ToString() => Name;
}
}
36 changes: 26 additions & 10 deletions tests/NATS.Client.TestUtilities/NatsServer.cs
Original file line number Diff line number Diff line change
@@ -85,6 +85,22 @@ private NatsServer(ITestOutputHelper outputHelper, NatsServerOpts opts)
_ => throw new ArgumentOutOfRangeException(),
};

public string ClientUrlWithAuth
{
get
{
if (!string.IsNullOrEmpty(Opts.ClientUrlUserName))
{
var uriBuilder = new UriBuilder(ClientUrl);
uriBuilder.UserName = Opts.ClientUrlUserName;
uriBuilder.Password = Opts.ClientUrlPassword;
return uriBuilder.ToString().TrimEnd('/');
}

return ClientUrl;
}
}

public int ConnectionPort
{
get
@@ -134,7 +150,7 @@ public static NatsServer StartWithTrace(ITestOutputHelper outputHelper)
public static NatsServer Start(ITestOutputHelper outputHelper, TransportType transportType) =>
Start(outputHelper, new NatsServerOptsBuilder().UseTransport(transportType).Build());

public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts opts, NatsOpts? clientOpts = default)
public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts opts, NatsOpts? clientOpts = default, bool useAuthInUrl = false)
{
NatsServer? server = null;
NatsConnection? nats = null;
@@ -144,7 +160,7 @@ public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts op
{
server = new NatsServer(outputHelper, opts);
server.StartServerProcess();
nats = server.CreateClientConnection(clientOpts ?? NatsOpts.Default, reTryCount: 3);
nats = server.CreateClientConnection(clientOpts ?? NatsOpts.Default, reTryCount: 3, useAuthInUrl: useAuthInUrl);
#pragma warning disable CA2012
return server;
}
@@ -335,13 +351,13 @@ public async ValueTask DisposeAsync()
return (client, proxy);
}

public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false, bool testLogger = true)
public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false, bool testLogger = true, bool useAuthInUrl = false)
{
for (var i = 0; i < reTryCount; i++)
{
try
{
var nats = new NatsConnection(ClientOpts(options ?? NatsOpts.Default, testLogger: testLogger));
var nats = new NatsConnection(ClientOpts(options ?? NatsOpts.Default, testLogger: testLogger, useAuthInUrl: useAuthInUrl));

try
{
@@ -376,7 +392,7 @@ public NatsConnectionPool CreatePooledClientConnection(NatsOpts opts)
return new NatsConnectionPool(4, ClientOpts(opts));
}

public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true)
public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true, bool useAuthInUrl = false)
{
var natsTlsOpts = Opts.EnableTls
? opts.TlsOpts with
@@ -392,7 +408,7 @@ public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true)
{
LoggerFactory = testLogger ? _loggerFactory : opts.LoggerFactory,
TlsOpts = natsTlsOpts,
Url = ClientUrl,
Url = useAuthInUrl ? ClientUrlWithAuth : ClientUrl,
};
}

@@ -464,7 +480,7 @@ public class NatsCluster : IAsyncDisposable
{
private readonly ITestOutputHelper _outputHelper;

public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, Action<int, NatsServerOptsBuilder>? configure = default)
public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, Action<int, NatsServerOptsBuilder>? configure = default, bool useAuthInUrl = false)
{
_outputHelper = outputHelper;

@@ -516,13 +532,13 @@ public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType,
}

_outputHelper.WriteLine($"Starting server 1...");
Server1 = NatsServer.Start(outputHelper, opts1);
Server1 = NatsServer.Start(outputHelper, opts1, useAuthInUrl: useAuthInUrl);

_outputHelper.WriteLine($"Starting server 2...");
Server2 = NatsServer.Start(outputHelper, opts2);
Server2 = NatsServer.Start(outputHelper, opts2, useAuthInUrl: useAuthInUrl);

_outputHelper.WriteLine($"Starting server 3...");
Server3 = NatsServer.Start(outputHelper, opts3);
Server3 = NatsServer.Start(outputHelper, opts3, useAuthInUrl: useAuthInUrl);
}

public NatsServer Server1 { get; }
23 changes: 23 additions & 0 deletions tests/NATS.Client.TestUtilities/NatsServerOpts.cs
Original file line number Diff line number Diff line change
@@ -31,6 +31,8 @@ public sealed class NatsServerOptsBuilder
private bool _serverDisposeReturnsPorts;
private bool _enableClustering;
private bool _trace;
private string? _clientUrlUserName;
private string? _clientUrlPassword;

public NatsServerOpts Build() => new()
{
@@ -40,6 +42,8 @@ public sealed class NatsServerOptsBuilder
TlsVerify = _tlsVerify,
EnableJetStream = _enableJetStream,
ServerName = _serverName,
ClientUrlUserName = _clientUrlUserName,
ClientUrlPassword = _clientUrlPassword,
TlsServerCertFile = _tlsServerCertFile,
TlsServerKeyFile = _tlsServerKeyFile,
TlsClientCertFile = _tlsClientCertFile,
@@ -110,6 +114,21 @@ public NatsServerOptsBuilder WithServerName(string serverName)
return this;
}

public NatsServerOptsBuilder WithClientUrlAuthentication(string userName, string password)
{
_clientUrlUserName = userName;
_clientUrlPassword = password;
return this;
}

public NatsServerOptsBuilder WithClientUrlAuthentication(string authInfo)
{
var infoParts = authInfo.Split(':');
_clientUrlUserName = infoParts.FirstOrDefault();
_clientUrlPassword = infoParts.ElementAtOrDefault(1);
return this;
}

public NatsServerOptsBuilder UseJetStream()
{
_enableJetStream = true;
@@ -176,6 +195,10 @@ public NatsServerOpts()

public bool ServerDisposeReturnsPorts { get; init; } = true;

public string? ClientUrlUserName { get; set; }

public string? ClientUrlPassword { get; set; }

public string? TlsClientCertFile { get; init; }

public string? TlsClientKeyFile { get; init; }