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; /// /// 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. /// 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(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(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(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(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(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("/"); } }