Files
natsdotnet/tests/NATS.Server.Auth.Tests/Accounts/AuthMechanismTests.cs
Joseph Doherty 36b9dfa654 refactor: extract NATS.Server.Auth.Tests project
Move 50 auth/accounts/permissions/JWT/NKey test files from
NATS.Server.Tests into a dedicated NATS.Server.Auth.Tests project.
Update namespaces, replace private GetFreePort/ReadUntilAsync helpers
with TestUtilities calls, replace Task.Delay with TaskCompletionSource
in test doubles, and add InternalsVisibleTo.

690 tests pass.
2026-03-12 15:54:07 -04:00

595 lines
18 KiB
C#

using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Protocol;
using NATS.Server.TestUtilities;
namespace NATS.Server.Auth.Tests.Accounts;
/// <summary>
/// Tests for authentication mechanisms: username/password, token, NKey-based auth,
/// no-auth-user fallback, multi-user, and AuthService orchestration.
/// Reference: Go auth_test.go — TestUserClone*, TestNoAuthUser, TestUserConnectionDeadline, etc.
/// Reference: Go accounts_test.go — TestAccountMapsUsers.
/// </summary>
public class AuthMechanismTests
{
private static async Task<(NatsServer server, int port, CancellationTokenSource cts)> StartServerAsync(NatsOptions options)
{
var port = TestPortAllocator.GetFreePort();
options.Port = port;
var server = new NatsServer(options, NullLoggerFactory.Instance);
var cts = new CancellationTokenSource();
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
return (server, port, cts);
}
private static bool ExceptionChainContains(Exception ex, string substring)
{
Exception? current = ex;
while (current != null)
{
if (current.Message.Contains(substring, StringComparison.OrdinalIgnoreCase))
return true;
current = current.InnerException;
}
return false;
}
// Go: TestUserCloneNilPermissions server/auth_test.go:34
[Fact]
public void User_with_nil_permissions()
{
var user = new User
{
Username = "foo",
Password = "bar",
};
user.Permissions.ShouldBeNull();
}
// Go: TestUserClone server/auth_test.go:53
[Fact]
public void User_with_permissions_has_correct_fields()
{
var user = new User
{
Username = "foo",
Password = "bar",
Permissions = new Permissions
{
Publish = new SubjectPermission { Allow = ["foo"] },
Subscribe = new SubjectPermission { Allow = ["bar"] },
},
};
user.Username.ShouldBe("foo");
user.Password.ShouldBe("bar");
user.Permissions.ShouldNotBeNull();
user.Permissions.Publish!.Allow![0].ShouldBe("foo");
user.Permissions.Subscribe!.Allow![0].ShouldBe("bar");
}
// Go: TestUserClonePermissionsNoLists server/auth_test.go:80
[Fact]
public void User_with_empty_permissions()
{
var user = new User
{
Username = "foo",
Password = "bar",
Permissions = new Permissions(),
};
user.Permissions!.Publish.ShouldBeNull();
user.Permissions!.Subscribe.ShouldBeNull();
}
// Go: TestNoAuthUser (token auth success) server/auth_test.go:225
[Fact]
public async Task Token_auth_success()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Authorization = "s3cr3t",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://s3cr3t@127.0.0.1:{port}",
});
await client.ConnectAsync();
await client.PingAsync();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: auth mechanism — token auth failure
[Fact]
public async Task Token_auth_failure_disconnects()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Authorization = "s3cr3t",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://wrongtoken@127.0.0.1:{port}",
MaxReconnectRetry = 0,
});
var ex = await Should.ThrowAsync<NatsException>(async () =>
{
await client.ConnectAsync();
await client.PingAsync();
});
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: auth mechanism — user/password success
[Fact]
public async Task UserPassword_auth_success()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Username = "admin",
Password = "secret",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://admin:secret@127.0.0.1:{port}",
});
await client.ConnectAsync();
await client.PingAsync();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: auth mechanism — user/password failure
[Fact]
public async Task UserPassword_auth_failure_disconnects()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Username = "admin",
Password = "secret",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://admin:wrong@127.0.0.1:{port}",
MaxReconnectRetry = 0,
});
var ex = await Should.ThrowAsync<NatsException>(async () =>
{
await client.ConnectAsync();
await client.PingAsync();
});
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
$"Expected 'Authorization Violation' in exception chain, but got: {ex}");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: TestNoAuthUser server/auth_test.go:225 — multi-user auth
[Fact]
public async Task MultiUser_auth_each_user_succeeds()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Users =
[
new User { Username = "alice", Password = "pass1" },
new User { Username = "bob", Password = "pass2" },
],
});
try
{
await using var alice = new NatsConnection(new NatsOpts
{
Url = $"nats://alice:pass1@127.0.0.1:{port}",
});
await using var bob = new NatsConnection(new NatsOpts
{
Url = $"nats://bob:pass2@127.0.0.1:{port}",
});
await alice.ConnectAsync();
await alice.PingAsync();
await bob.ConnectAsync();
await bob.PingAsync();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: TestNoAuthUser server/auth_test.go:225 — wrong user password
[Fact]
public async Task MultiUser_wrong_password_fails()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Users =
[
new User { Username = "alice", Password = "pass1" },
],
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://alice:wrong@127.0.0.1:{port}",
MaxReconnectRetry = 0,
});
var ex = await Should.ThrowAsync<NatsException>(async () =>
{
await client.ConnectAsync();
await client.PingAsync();
});
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
$"Expected 'Authorization Violation', but got: {ex}");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: auth mechanism — no credentials with auth required
[Fact]
public async Task No_credentials_when_auth_required_disconnects()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Authorization = "s3cr3t",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://127.0.0.1:{port}",
MaxReconnectRetry = 0,
});
var ex = await Should.ThrowAsync<NatsException>(async () =>
{
await client.ConnectAsync();
await client.PingAsync();
});
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
$"Expected 'Authorization Violation', but got: {ex}");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: auth mechanism — no auth configured allows all
[Fact]
public async Task No_auth_configured_allows_all()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions());
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://127.0.0.1:{port}",
});
await client.ConnectAsync();
await client.PingAsync();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: TestNoAuthUser server/auth_test.go:225 — no_auth_user fallback
[Fact]
public async Task NoAuthUser_fallback_allows_unauthenticated_connection()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Users =
[
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
new User { Username = "bar", Password = "pwd2", Account = "BAR" },
],
NoAuthUser = "foo",
});
try
{
// Connect without credentials — should use no_auth_user "foo"
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://127.0.0.1:{port}",
});
await client.ConnectAsync();
await client.PingAsync();
// Explicit auth also still works
await using var bar = new NatsConnection(new NatsOpts
{
Url = $"nats://bar:pwd2@127.0.0.1:{port}",
});
await bar.ConnectAsync();
await bar.PingAsync();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: TestNoAuthUser server/auth_test.go:225 — invalid pwd with no_auth_user still fails
[Fact]
public async Task NoAuthUser_wrong_password_still_fails()
{
var (server, port, cts) = await StartServerAsync(new NatsOptions
{
Users =
[
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
new User { Username = "bar", Password = "pwd2", Account = "BAR" },
],
NoAuthUser = "foo",
});
try
{
await using var client = new NatsConnection(new NatsOpts
{
Url = $"nats://bar:wrong@127.0.0.1:{port}",
MaxReconnectRetry = 0,
});
var ex = await Should.ThrowAsync<NatsException>(async () =>
{
await client.ConnectAsync();
await client.PingAsync();
});
ExceptionChainContains(ex, "Authorization Violation").ShouldBeTrue(
$"Expected auth violation, got: {ex}");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// Go: AuthService — tests the build logic for auth service
[Fact]
public void AuthService_build_with_no_auth_returns_not_required()
{
var authService = AuthService.Build(new NatsOptions());
authService.IsAuthRequired.ShouldBeFalse();
authService.NonceRequired.ShouldBeFalse();
}
// Go: AuthService — tests the build logic for token auth
[Fact]
public void AuthService_build_with_token_marks_auth_required()
{
var authService = AuthService.Build(new NatsOptions { Authorization = "secret" });
authService.IsAuthRequired.ShouldBeTrue();
authService.NonceRequired.ShouldBeFalse();
}
// Go: AuthService — tests the build logic for user/password auth
[Fact]
public void AuthService_build_with_user_password_marks_auth_required()
{
var authService = AuthService.Build(new NatsOptions
{
Username = "admin",
Password = "secret",
});
authService.IsAuthRequired.ShouldBeTrue();
authService.NonceRequired.ShouldBeFalse();
}
// Go: AuthService — tests the build logic for nkey auth
[Fact]
public void AuthService_build_with_nkeys_marks_nonce_required()
{
var authService = AuthService.Build(new NatsOptions
{
NKeys = [new NKeyUser { Nkey = "UABC123" }],
});
authService.IsAuthRequired.ShouldBeTrue();
authService.NonceRequired.ShouldBeTrue();
}
// Go: AuthService — tests the build logic for multi-user auth
[Fact]
public void AuthService_build_with_users_marks_auth_required()
{
var authService = AuthService.Build(new NatsOptions
{
Users = [new User { Username = "alice", Password = "pass" }],
});
authService.IsAuthRequired.ShouldBeTrue();
}
// Go: AuthService.Authenticate — token match
[Fact]
public void AuthService_authenticate_token_success()
{
var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
var result = authService.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Token = "mytoken" },
Nonce = [],
});
result.ShouldNotBeNull();
result.Identity.ShouldBe("token");
}
// Go: AuthService.Authenticate — token mismatch
[Fact]
public void AuthService_authenticate_token_failure()
{
var authService = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
var result = authService.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Token = "wrong" },
Nonce = [],
});
result.ShouldBeNull();
}
// Go: AuthService.Authenticate — user/password match
[Fact]
public void AuthService_authenticate_user_password_success()
{
var authService = AuthService.Build(new NatsOptions
{
Users = [new User { Username = "alice", Password = "pass", Account = "acct-a" }],
});
var result = authService.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "alice", Password = "pass" },
Nonce = [],
});
result.ShouldNotBeNull();
result.Identity.ShouldBe("alice");
result.AccountName.ShouldBe("acct-a");
}
// Go: AuthService.Authenticate — user/password mismatch
[Fact]
public void AuthService_authenticate_user_password_failure()
{
var authService = AuthService.Build(new NatsOptions
{
Users = [new User { Username = "alice", Password = "pass" }],
});
var result = authService.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions { Username = "alice", Password = "wrong" },
Nonce = [],
});
result.ShouldBeNull();
}
// Go: AuthService.Authenticate — no auth user fallback
[Fact]
public void AuthService_authenticate_no_auth_user_fallback()
{
var authService = AuthService.Build(new NatsOptions
{
Users =
[
new User { Username = "foo", Password = "pwd1", Account = "FOO" },
],
NoAuthUser = "foo",
});
// No credentials provided — should fall back to no_auth_user
var result = authService.Authenticate(new ClientAuthContext
{
Opts = new ClientOptions(),
Nonce = [],
});
result.ShouldNotBeNull();
result.Identity.ShouldBe("foo");
result.AccountName.ShouldBe("FOO");
}
// Go: AuthService.GenerateNonce — nonce generation
[Fact]
public void AuthService_generates_unique_nonces()
{
var authService = AuthService.Build(new NatsOptions
{
NKeys = [new NKeyUser { Nkey = "UABC" }],
});
var nonce1 = authService.GenerateNonce();
var nonce2 = authService.GenerateNonce();
nonce1.Length.ShouldBe(11);
nonce2.Length.ShouldBe(11);
// Extremely unlikely to be the same
nonce1.ShouldNotBe(nonce2);
}
// Go: AuthService.EncodeNonce — nonce encoding
[Fact]
public void AuthService_nonce_encoding_is_url_safe_base64()
{
var authService = AuthService.Build(new NatsOptions());
var nonce = new byte[] { 0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5 };
var encoded = authService.EncodeNonce(nonce);
// Should not contain standard base64 padding or non-URL-safe characters
encoded.ShouldNotContain("=");
encoded.ShouldNotContain("+");
encoded.ShouldNotContain("/");
}
}