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;
Expand Down Expand Up @@ -76,7 +75,7 @@ public NatsConnection()
public NatsConnection(NatsOpts opts)
{
_logger = opts.LoggerFactory.CreateLogger<NatsConnection>();
Opts = opts;
Opts = ReadUserInfoFromConnectionString(opts);
mtmk marked this conversation as resolved.
Show resolved Hide resolved
ConnectionState = NatsConnectionState.Closed;
_waitForOpenConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_disposedCancellationTokenSource = new CancellationTokenSource();
Expand Down Expand Up @@ -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
Mrxx99 marked this conversation as resolved.
Show resolved Hide resolved
}
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");
Expand Down
4 changes: 2 additions & 2 deletions src/NATS.Client.Core/NatsOpts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
32 changes: 26 additions & 6 deletions tests/NATS.Client.Core.Tests/ClusterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
44 changes: 36 additions & 8 deletions tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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");

Expand All @@ -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();
Expand Down Expand Up @@ -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

Expand All @@ -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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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

View workflow job for this annotation

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; }
Expand All @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand All @@ -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,
};
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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; }
Expand Down
23 changes: 23 additions & 0 deletions tests/NATS.Client.TestUtilities/NatsServerOpts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -40,6 +42,8 @@ public sealed class NatsServerOptsBuilder
TlsVerify = _tlsVerify,
EnableJetStream = _enableJetStream,
ServerName = _serverName,
ClientUrlUserName = _clientUrlUserName,
ClientUrlPassword = _clientUrlPassword,
TlsServerCertFile = _tlsServerCertFile,
TlsServerKeyFile = _tlsServerKeyFile,
TlsClientCertFile = _tlsClientCertFile,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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; }
Expand Down