From aedc9aae822ed1ed2cb33a4374ce4db6938e0b13 Mon Sep 17 00:00:00 2001 From: Mrxx99 <33566379+Mrxx99@users.noreply.github.com> Date: Tue, 5 Nov 2024 08:42:50 +0100 Subject: [PATCH] Add support for auth in URL (#670) * Add support for auth in URL * formatting * undo not needed change * address review * use first uri to get auth information * set username on multiple uris to the actual used one + better redaction * Add tests and docs * Add docs to the client * Format * Implement URL encoding for user credentials * Refactor authentication precedence and URL redaction handling Redact passwords and tokens at the `NatsUri` level to preserve the original URIs, especially when used in WebSocket connections. This change aims to maintain backward compatibility and prevent disruptions in WebSocket connections that may rely on tokens or user-password pairs for proxy-level authentication. * Remove unused test params * Refactor redacted URI handling in NatsUri class Modified the NatsUri class to store the redacted URI as a string instead of a Uri object. This change simplifies the ToString method and ensures that sensitive information like user credentials is consistently redacted for logging purposes. Redacted string is created once in the ctor and avoiding Uri.ToString() calls being made everytime it's needed. * Move read URL credentials into NatsOpts --------- Co-authored-by: Ziya Suzen --- src/NATS.Client.Core/Internal/NatsUri.cs | 22 +++- src/NATS.Client.Core/NatsConnection.cs | 3 +- src/NATS.Client.Core/NatsOpts.cs | 65 +++++++++- src/NATS.Client.Simplified/NatsClient.cs | 22 +++- tests/NATS.Client.Core.Tests/ClusterTests.cs | 32 ++++- .../NatsConnectionTest.Auth.cs | 44 +++++-- .../OptsUrlTests.cs | 121 ++++++++++++++++++ tests/NATS.Client.TestUtilities/NatsServer.cs | 36 ++++-- .../NatsServerOpts.cs | 23 ++++ 9 files changed, 333 insertions(+), 35 deletions(-) create mode 100644 tests/NATS.Client.CoreUnit.Tests/OptsUrlTests.cs diff --git a/src/NATS.Client.Core/Internal/NatsUri.cs b/src/NATS.Client.Core/Internal/NatsUri.cs index 64d3ea4c2..02144d50c 100644 --- a/src/NATS.Client.Core/Internal/NatsUri.cs +++ b/src/NATS.Client.Core/Internal/NatsUri.cs @@ -4,6 +4,8 @@ internal sealed class NatsUri : IEquatable { public const string DefaultScheme = "nats"; + private readonly string _redacted; + public NatsUri(string urlString, bool isSeed, string defaultScheme = DefaultScheme) { IsSeed = isSeed; @@ -38,6 +40,21 @@ public NatsUri(string urlString, bool isSeed, string defaultScheme = DefaultSche } Uri = uriBuilder.Uri; + + // Redact user/password or token from the URI string for logging + if (uriBuilder.UserName is { Length: > 0 }) + { + if (uriBuilder.Password is { Length: > 0 }) + { + uriBuilder.Password = "***"; + } + else + { + uriBuilder.UserName = "***"; + } + } + + _redacted = IsWebSocket && Uri.AbsolutePath != "/" ? uriBuilder.Uri.ToString() : uriBuilder.Uri.ToString().Trim('/'); } public Uri Uri { get; } @@ -63,10 +80,7 @@ public NatsUri CloneWith(string host, int? port = default) return new NatsUri(newUri, IsSeed); } - public override string ToString() - { - return IsWebSocket && Uri.AbsolutePath != "/" ? Uri.ToString() : Uri.ToString().Trim('/'); - } + public override string ToString() => _redacted; public override int GetHashCode() => Uri.GetHashCode(); diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 85862e7b7..0601ea6d7 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -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(); - Opts = opts; + Opts = opts.ReadUserInfoFromConnectionString(); ConnectionState = NatsConnectionState.Closed; _waitForOpenConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _disposedCancellationTokenSource = new CancellationTokenSource(); diff --git a/src/NATS.Client.Core/NatsOpts.cs b/src/NATS.Client.Core/NatsOpts.cs index 2c2245b2a..5dc7b2083 100644 --- a/src/NATS.Client.Core/NatsOpts.cs +++ b/src/NATS.Client.Core/NatsOpts.cs @@ -14,6 +14,28 @@ public sealed record NatsOpts { public static readonly NatsOpts Default = new(); + /// + /// NATS server URL to connect to. (default: nats://localhost:4222) + /// + /// + /// + /// You can set more than one server as seed servers in a comma-separated list. + /// The client will randomly select a server from the list to connect to unless + /// (which is false by default) is set to true. + /// + /// + /// User-password or token authentication can be set in the URL. + /// For example, nats://derek:s3cr3t@localhost:4222 or nats://token@localhost:4222. + /// You can also set the username and password or token separately using ; + /// however, if both are set, the will take precedence. + /// You should URL-encode the username and password or token if they contain special characters. + /// + /// + /// If multiple servers are specified and user-password or token authentication is used in the URL, + /// only the credentials in the first server URL will be used; credentials in the remaining server + /// URLs will be ignored. + /// + /// public string Url { get; init; } = "nats://localhost:4222"; public string Name { get; init; } = "NATS .NET Client"; @@ -117,11 +139,50 @@ public sealed record NatsOpts /// 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(); } + + internal NatsOpts ReadUserInfoFromConnectionString() + { + // Setting credentials in options takes precedence over URL credentials + if (AuthOpts.Username is { Length: > 0 } || AuthOpts.Password is { Length: > 0 } || AuthOpts.Token is { Length: > 0 }) + { + return this; + } + + var natsUri = GetSeedUris(suppressRandomization: true).First(); + var uriBuilder = new UriBuilder(natsUri.Uri); + + if (uriBuilder.UserName is not { Length: > 0 }) + { + return this; + } + + if (uriBuilder.Password is { Length: > 0 }) + { + return this with + { + AuthOpts = AuthOpts with + { + Username = Uri.UnescapeDataString(uriBuilder.UserName), + Password = Uri.UnescapeDataString(uriBuilder.Password), + }, + }; + } + else + { + return this with + { + AuthOpts = AuthOpts with + { + Token = Uri.UnescapeDataString(uriBuilder.UserName), + }, + }; + } + } } diff --git a/src/NATS.Client.Simplified/NatsClient.cs b/src/NATS.Client.Simplified/NatsClient.cs index 87de0d6c0..5cc9dc383 100644 --- a/src/NATS.Client.Simplified/NatsClient.cs +++ b/src/NATS.Client.Simplified/NatsClient.cs @@ -11,9 +11,25 @@ public class NatsClient : INatsClient /// /// Initializes a new instance of the class. /// - /// NATS server URL - /// Client name - /// Credentials filepath + /// NATS server URL to connect to. (default: nats://localhost:4222) + /// Client name. (default: NATS .NET Client) + /// Credentials filepath. + /// + /// + /// You can set more than one server as seed servers in a comma-separated list in the . + /// The client will randomly select a server from the list to connect. + /// + /// + /// User-password or token authentication can be set in the . + /// For example, nats://derek:s3cr3t@localhost:4222 or nats://token@localhost:4222. + /// You should URL-encode the username and password or token if they contain special characters. + /// + /// + /// If multiple servers are specified and user-password or token authentication is used in the , + /// only the credentials in the first server URL will be used; credentials in the remaining server + /// URLs will be ignored. + /// + /// public NatsClient( string url = "nats://localhost:4222", string name = "NATS .NET Client", diff --git a/tests/NATS.Client.Core.Tests/ClusterTests.cs b/tests/NATS.Client.Core.Tests/ClusterTests.cs index 0ef8be7fc..05b3f2cec 100644 --- a/tests/NATS.Client.Core.Tests/ClusterTests.cs +++ b/tests/NATS.Client.Core.Tests/ClusterTests.cs @@ -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 { diff --git a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs index 16aad262b..11c6ac559 100644 --- a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs +++ b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs @@ -12,6 +12,15 @@ public static IEnumerable GetAuthConfigs() 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 @@ NatsOpts.Default with }), }; + 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 @@ public async Task UserCredentialAuthTest(Auth auth) 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 @@ public async Task UserCredentialAuthTest(Auth auth) 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 Retry.Until( 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 @@ await Retry.Until( public class Auth { - public Auth(string name, string serverConfig, NatsOpts clientOpts) + public Auth(string name, string serverConfig, NatsOpts clientOpts, string? urlAuth = null) { Name = name; ServerConfig = serverConfig; ClientOpts = clientOpts; + UrlAuth = urlAuth; } public string Name { get; } @@ -175,6 +201,8 @@ public Auth(string name, string serverConfig, NatsOpts clientOpts) public NatsOpts ClientOpts { get; } + public string? UrlAuth { get; } + public override string ToString() => Name; } } diff --git a/tests/NATS.Client.CoreUnit.Tests/OptsUrlTests.cs b/tests/NATS.Client.CoreUnit.Tests/OptsUrlTests.cs new file mode 100644 index 000000000..e4144d179 --- /dev/null +++ b/tests/NATS.Client.CoreUnit.Tests/OptsUrlTests.cs @@ -0,0 +1,121 @@ +namespace NATS.Client.Core.Tests; + +public class OptsUrlTests +{ + [Fact] + public void Default_URL() + { + var opts = new NatsConnection().Opts; + Assert.Equal("nats://localhost:4222", opts.Url); + } + + [Fact] + public void Redact_URL_user_password() + { + var natsUri = new NatsUri("u:p@host", true); + Assert.Equal("nats://u:***@host:4222", natsUri.ToString()); + Assert.Equal("u:p", natsUri.Uri.UserInfo); + } + + [Fact] + public void Redact_URL_token() + { + var natsUri = new NatsUri("t@host", true); + Assert.Equal("nats://***@host:4222", natsUri.ToString()); + Assert.Equal("t", natsUri.Uri.UserInfo); + } + + [Theory] + [InlineData("host1", "nats://host1:4222", null, null, null)] + [InlineData("host1:1234", "nats://host1:1234", null, null, null)] + [InlineData("tls://host1", "tls://host1:4222", null, null, null)] + [InlineData("u:p@host1:1234", "nats://u:***@host1:1234", "u", "p", null)] + [InlineData("t@host1:1234", "nats://***@host1:1234", null, null, "t")] + [InlineData("host1,host2", "nats://host1:4222,nats://host2:4222", null, null, null)] + [InlineData("u:p@host1,host2", "nats://u:***@host1:4222,nats://host2:4222", "u", "p", null)] + [InlineData("u:p@host1,x@host2", "nats://u:***@host1:4222,nats://***@host2:4222", "u", "p", null)] + [InlineData("t@host1,x:x@host2", "nats://***@host1:4222,nats://x:***@host2:4222", null, null, "t")] + [InlineData("u:p@host1,host2,host3", "nats://u:***@host1:4222,nats://host2:4222,nats://host3:4222", "u", "p", null)] + [InlineData("t@host1,@host2,host3", "nats://***@host1:4222,nats://host2:4222,nats://host3:4222", null, null, "t")] + public void URL_parts(string url, string expected, string? user, string? pass, string? token) + { + var opts = new NatsConnection(new NatsOpts { Url = url }).Opts; + Assert.Equal(expected, GetUrisAsRedactedString(opts)); + Assert.Equal(user, opts.AuthOpts.Username); + Assert.Equal(pass, opts.AuthOpts.Password); + Assert.Equal(token, opts.AuthOpts.Token); + } + + [Theory] + [InlineData("u:p@host1:1234", "nats://u:***@host1:1234")] + [InlineData("t@host1:1234", "nats://***@host1:1234")] + public void URL_should_not_override_auth_options(string url, string expected) + { + var opts = new NatsConnection(new NatsOpts + { + Url = url, + AuthOpts = new NatsAuthOpts + { + Username = "shouldn't override username", + Password = "shouldn't override password", + Token = "shouldn't override token", + }, + }).Opts; + Assert.Equal(expected, GetUrisAsRedactedString(opts)); + Assert.Equal("shouldn't override username", opts.AuthOpts.Username); + Assert.Equal("shouldn't override password", opts.AuthOpts.Password); + Assert.Equal("shouldn't override token", opts.AuthOpts.Token); + } + + [Fact] + public void URL_escape_user_password() + { + var opts = new NatsConnection(new NatsOpts { Url = "nats://u%2C:p%2C@host1,host2" }).Opts; + Assert.Equal("nats://u%2C:***@host1:4222,nats://host2:4222", GetUrisAsRedactedString(opts)); + Assert.Equal("u,", opts.AuthOpts.Username); + Assert.Equal("p,", opts.AuthOpts.Password); + Assert.Null(opts.AuthOpts.Token); + + var uris = opts.GetSeedUris(true); + uris[0].Uri.Scheme.Should().Be("nats"); + uris[0].Uri.Host.Should().Be("host1"); + uris[0].Uri.Port.Should().Be(4222); + uris[0].Uri.UserInfo.Should().Be("u%2C:p%2C"); + uris[1].Uri.Scheme.Should().Be("nats"); + uris[1].Uri.Host.Should().Be("host2"); + uris[1].Uri.Port.Should().Be(4222); + uris[1].Uri.UserInfo.Should().Be(string.Empty); + } + + [Fact] + public void URL_escape_token() + { + var opts = new NatsConnection(new NatsOpts { Url = "nats://t%2C@host1,nats://t%2C@host2" }).Opts; + Assert.Equal("nats://***@host1:4222,nats://***@host2:4222", GetUrisAsRedactedString(opts)); + Assert.Null(opts.AuthOpts.Username); + Assert.Null(opts.AuthOpts.Password); + Assert.Equal("t,", opts.AuthOpts.Token); + + var uris = opts.GetSeedUris(true); + uris[0].Uri.Scheme.Should().Be("nats"); + uris[0].Uri.Host.Should().Be("host1"); + uris[0].Uri.Port.Should().Be(4222); + uris[0].Uri.UserInfo.Should().Be("t%2C"); + uris[1].Uri.Scheme.Should().Be("nats"); + uris[1].Uri.Host.Should().Be("host2"); + uris[1].Uri.Port.Should().Be(4222); + uris[1].Uri.UserInfo.Should().Be("t%2C"); + } + + [Fact] + public void Keep_URL_wss_path_and_query_string() + { + var opts = new NatsConnection(new NatsOpts { Url = "wss://t%2C@host1/path1/path2?q1=1" }).Opts; + Assert.Equal("wss://***@host1/path1/path2?q1=1", GetUrisAsRedactedString(opts)); + Assert.Null(opts.AuthOpts.Username); + Assert.Null(opts.AuthOpts.Password); + Assert.Equal("t,", opts.AuthOpts.Token); + } + + private static string GetUrisAsRedactedString(NatsOpts opts) => string.Join(",", opts.GetSeedUris(true).Select(u => u.ToString())); +} diff --git a/tests/NATS.Client.TestUtilities/NatsServer.cs b/tests/NATS.Client.TestUtilities/NatsServer.cs index 83cb90001..3de63b5d0 100644 --- a/tests/NATS.Client.TestUtilities/NatsServer.cs +++ b/tests/NATS.Client.TestUtilities/NatsServer.cs @@ -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? configure = default) + public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, Action? 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; } diff --git a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs index 43c69ba26..2f2b6fc3a 100644 --- a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs +++ b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs @@ -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; }