# Authentication Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

**Goal:** Add username/password, token, and NKey authentication with per-user permissions and core account isolation to the .NET NATS server.

**Architecture:** Strategy pattern — `IAuthenticator` interface with concrete implementations per mechanism, orchestrated by `AuthService` in priority order. Per-account `SubList` isolation. Permission checks on PUB/SUB hot paths.

**Tech Stack:** .NET 10, NATS.NKeys (Ed25519), BCrypt.Net-Next (password hashing), xUnit + Shouldly (tests)

**Design doc:** `docs/plans/2026-02-22-authentication-design.md`

---

### Task 0: Add NuGet packages and update project references

**Files:**
- Modify: `Directory.Packages.props`
- Modify: `src/NATS.Server/NATS.Server.csproj`
- Modify: `tests/NATS.Server.Tests/NATS.Server.Tests.csproj`

**Step 1: Add package versions to Directory.Packages.props**

Add these entries to the `<ItemGroup>` in `Directory.Packages.props`, after the Logging section:

```xml
    <!-- Authentication -->
    <PackageVersion Include="NATS.NKeys" Version="1.0.0-preview.3" />
    <PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
```

**Step 2: Add package references to NATS.Server.csproj**

Add to the `<ItemGroup>` in `src/NATS.Server/NATS.Server.csproj`:

```xml
    <PackageReference Include="NATS.NKeys" />
    <PackageReference Include="BCrypt.Net-Next" />
```

**Step 3: Add NATS.NKeys to test project**

Add to the `<ItemGroup>` (package references) in `tests/NATS.Server.Tests/NATS.Server.Tests.csproj`:

```xml
    <PackageReference Include="NATS.NKeys" />
```

**Step 4: Verify build**

Run: `dotnet build`
Expected: Build succeeds with no errors.

**Step 5: Commit**

```bash
git add Directory.Packages.props src/NATS.Server/NATS.Server.csproj tests/NATS.Server.Tests/NATS.Server.Tests.csproj
git commit -m "chore: add NATS.NKeys and BCrypt.Net-Next packages for authentication"
```

---

### Task 1: Add auth fields to protocol types (ServerInfo, ClientOptions, error constants)

**Files:**
- Modify: `src/NATS.Server/Protocol/NatsProtocol.cs:23-28` (add error constants)
- Modify: `src/NATS.Server/Protocol/NatsProtocol.cs:31-64` (ServerInfo — add AuthRequired, Nonce)
- Modify: `src/NATS.Server/Protocol/NatsProtocol.cs:66-94` (ClientOptions — add auth credential fields)

**Step 1: Write a failing test for auth fields in ClientOptions**

Create `tests/NATS.Server.Tests/AuthProtocolTests.cs`:

```csharp
using System.Text.Json;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class AuthProtocolTests
{
    [Fact]
    public void ClientOptions_deserializes_auth_fields()
    {
        var json = """{"user":"alice","pass":"secret","auth_token":"mytoken","nkey":"UABC","sig":"base64sig"}""";
        var opts = JsonSerializer.Deserialize<ClientOptions>(json);

        opts.ShouldNotBeNull();
        opts.Username.ShouldBe("alice");
        opts.Password.ShouldBe("secret");
        opts.Token.ShouldBe("mytoken");
        opts.Nkey.ShouldBe("UABC");
        opts.Sig.ShouldBe("base64sig");
    }

    [Fact]
    public void ServerInfo_serializes_auth_required_and_nonce()
    {
        var info = new ServerInfo
        {
            ServerId = "test",
            ServerName = "test",
            Version = "0.1.0",
            Host = "127.0.0.1",
            Port = 4222,
            AuthRequired = true,
            Nonce = "abc123",
        };

        var json = JsonSerializer.Serialize(info);
        json.ShouldContain("\"auth_required\":true");
        json.ShouldContain("\"nonce\":\"abc123\"");
    }

    [Fact]
    public void ServerInfo_omits_nonce_when_null()
    {
        var info = new ServerInfo
        {
            ServerId = "test",
            ServerName = "test",
            Version = "0.1.0",
            Host = "127.0.0.1",
            Port = 4222,
        };

        var json = JsonSerializer.Serialize(info);
        json.ShouldNotContain("nonce");
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthProtocolTests" -v normal`
Expected: FAIL — `Username`, `Password`, `Token`, `Nkey`, `Sig` properties don't exist on `ClientOptions`. `AuthRequired`, `Nonce` don't exist on `ServerInfo`.

**Step 3: Add error constants to NatsProtocol**

In `src/NATS.Server/Protocol/NatsProtocol.cs`, add after line 28 (`ErrInvalidSubject`):

```csharp
    public const string ErrAuthorizationViolation = "Authorization Violation";
    public const string ErrAuthTimeout = "Authentication Timeout";
    public const string ErrPermissionsPublish = "Permissions Violation for Publish";
    public const string ErrPermissionsSubscribe = "Permissions Violation for Subscription";
```

**Step 4: Add auth fields to ServerInfo**

In `src/NATS.Server/Protocol/NatsProtocol.cs`, add after the `ClientIp` property (line 63):

```csharp
    [JsonPropertyName("auth_required")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public bool AuthRequired { get; set; }

    [JsonPropertyName("nonce")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? Nonce { get; set; }
```

**Step 5: Add auth fields to ClientOptions**

In `src/NATS.Server/Protocol/NatsProtocol.cs`, add after the `NoResponders` property (line 93):

```csharp
    [JsonPropertyName("user")]
    public string? Username { get; set; }

    [JsonPropertyName("pass")]
    public string? Password { get; set; }

    [JsonPropertyName("auth_token")]
    public string? Token { get; set; }

    [JsonPropertyName("nkey")]
    public string? Nkey { get; set; }

    [JsonPropertyName("sig")]
    public string? Sig { get; set; }
```

**Step 6: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthProtocolTests" -v normal`
Expected: All 3 tests PASS.

**Step 7: Run full test suite for regressions**

Run: `dotnet test`
Expected: All existing tests still pass.

**Step 8: Commit**

```bash
git add src/NATS.Server/Protocol/NatsProtocol.cs tests/NATS.Server.Tests/AuthProtocolTests.cs
git commit -m "feat: add auth fields to ServerInfo and ClientOptions protocol types"
```

---

### Task 2: Add auth configuration to NatsOptions

**Files:**
- Modify: `src/NATS.Server/NatsOptions.cs`

**Step 1: Write failing test**

Add to `tests/NATS.Server.Tests/AuthProtocolTests.cs`:

```csharp
using NATS.Server;

// ... add this test class to the same file or a new AuthConfigTests.cs ...

public class AuthConfigTests
{
    [Fact]
    public void NatsOptions_has_auth_fields_with_defaults()
    {
        var opts = new NatsOptions();

        opts.Username.ShouldBeNull();
        opts.Password.ShouldBeNull();
        opts.Authorization.ShouldBeNull();
        opts.Users.ShouldBeNull();
        opts.NKeys.ShouldBeNull();
        opts.NoAuthUser.ShouldBeNull();
        opts.AuthTimeout.ShouldBe(TimeSpan.FromSeconds(1));
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthConfigTests" -v normal`
Expected: FAIL — properties don't exist.

**Step 3: Create auth model types and update NatsOptions**

Create `src/NATS.Server/Auth/User.cs`:

```csharp
using NATS.Server.Auth;

namespace NATS.Server.Auth;

public sealed class User
{
    public required string Username { get; init; }
    public required string Password { get; init; }
    public Permissions? Permissions { get; init; }
    public string? Account { get; init; }
    public DateTimeOffset? ConnectionDeadline { get; init; }
}
```

Create `src/NATS.Server/Auth/NKeyUser.cs`:

```csharp
namespace NATS.Server.Auth;

public sealed class NKeyUser
{
    public required string Nkey { get; init; }
    public Permissions? Permissions { get; init; }
    public string? Account { get; init; }
    public string? SigningKey { get; init; }
}
```

Create `src/NATS.Server/Auth/Permissions.cs`:

```csharp
namespace NATS.Server.Auth;

public sealed class Permissions
{
    public SubjectPermission? Publish { get; init; }
    public SubjectPermission? Subscribe { get; init; }
    public ResponsePermission? Response { get; init; }
}

public sealed class SubjectPermission
{
    public IReadOnlyList<string>? Allow { get; init; }
    public IReadOnlyList<string>? Deny { get; init; }
}

public sealed class ResponsePermission
{
    public int MaxMsgs { get; init; }
    public TimeSpan Expires { get; init; }
}
```

Update `src/NATS.Server/NatsOptions.cs` to add auth fields:

```csharp
using NATS.Server.Auth;

namespace NATS.Server;

public sealed class NatsOptions
{
    public string Host { get; set; } = "0.0.0.0";
    public int Port { get; set; } = 4222;
    public string? ServerName { get; set; }
    public int MaxPayload { get; set; } = 1024 * 1024; // 1MB
    public int MaxControlLine { get; set; } = 4096;
    public int MaxConnections { get; set; } = 65536;
    public TimeSpan PingInterval { get; set; } = TimeSpan.FromMinutes(2);
    public int MaxPingsOut { get; set; } = 2;

    // Simple auth (single user)
    public string? Username { get; set; }
    public string? Password { get; set; }
    public string? Authorization { get; set; }

    // Multiple users/nkeys
    public IReadOnlyList<User>? Users { get; set; }
    public IReadOnlyList<NKeyUser>? NKeys { get; set; }

    // Default/fallback
    public string? NoAuthUser { get; set; }

    // Auth timing
    public TimeSpan AuthTimeout { get; set; } = TimeSpan.FromSeconds(1);
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthConfigTests" -v normal`
Expected: PASS.

**Step 5: Run full test suite**

Run: `dotnet test`
Expected: All tests pass.

**Step 6: Commit**

```bash
git add src/NATS.Server/Auth/ src/NATS.Server/NatsOptions.cs tests/NATS.Server.Tests/AuthProtocolTests.cs
git commit -m "feat: add auth model types (User, NKeyUser, Permissions) and auth config to NatsOptions"
```

---

### Task 3: Implement Account type with per-account SubList

**Files:**
- Create: `src/NATS.Server/Auth/Account.cs`
- Test: `tests/NATS.Server.Tests/AccountTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/AccountTests.cs`:

```csharp
using NATS.Server.Auth;
using NATS.Server.Subscriptions;

namespace NATS.Server.Tests;

public class AccountTests
{
    [Fact]
    public void Account_has_name_and_own_sublist()
    {
        var account = new Account("test-account");

        account.Name.ShouldBe("test-account");
        account.SubList.ShouldNotBeNull();
        account.SubList.Count.ShouldBe(0u);
    }

    [Fact]
    public void Account_tracks_clients()
    {
        var account = new Account("test");

        account.ClientCount.ShouldBe(0);
        account.AddClient(1);
        account.ClientCount.ShouldBe(1);
        account.RemoveClient(1);
        account.ClientCount.ShouldBe(0);
    }

    [Fact]
    public void GlobalAccount_has_default_name()
    {
        Account.GlobalAccountName.ShouldBe("$G");
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AccountTests" -v normal`
Expected: FAIL — `Account` class doesn't exist.

**Step 3: Implement Account**

Create `src/NATS.Server/Auth/Account.cs`:

```csharp
using System.Collections.Concurrent;
using NATS.Server.Subscriptions;

namespace NATS.Server.Auth;

public sealed class Account : IDisposable
{
    public const string GlobalAccountName = "$G";

    public string Name { get; }
    public SubList SubList { get; } = new();
    public Permissions? DefaultPermissions { get; set; }

    private readonly ConcurrentDictionary<ulong, byte> _clients = new();

    public Account(string name)
    {
        Name = name;
    }

    public int ClientCount => _clients.Count;

    public void AddClient(ulong clientId) => _clients[clientId] = 0;

    public void RemoveClient(ulong clientId) => _clients.TryRemove(clientId, out _);

    public void Dispose() => SubList.Dispose();
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AccountTests" -v normal`
Expected: All 3 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/Account.cs tests/NATS.Server.Tests/AccountTests.cs
git commit -m "feat: add Account type with per-account SubList and client tracking"
```

---

### Task 4: Implement TokenAuthenticator

**Files:**
- Create: `src/NATS.Server/Auth/IAuthenticator.cs`
- Create: `src/NATS.Server/Auth/AuthResult.cs`
- Create: `src/NATS.Server/Auth/TokenAuthenticator.cs`
- Test: `tests/NATS.Server.Tests/TokenAuthenticatorTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/TokenAuthenticatorTests.cs`:

```csharp
using NATS.Server.Auth;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class TokenAuthenticatorTests
{
    [Fact]
    public void Returns_result_for_correct_token()
    {
        var auth = new TokenAuthenticator("secret-token");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "secret-token" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("token");
    }

    [Fact]
    public void Returns_null_for_wrong_token()
    {
        var auth = new TokenAuthenticator("secret-token");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "wrong-token" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_when_no_token_provided()
    {
        var auth = new TokenAuthenticator("secret-token");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions(),
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_for_different_length_token()
    {
        var auth = new TokenAuthenticator("secret-token");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "short" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~TokenAuthenticatorTests" -v normal`
Expected: FAIL — types don't exist.

**Step 3: Implement interfaces and TokenAuthenticator**

Create `src/NATS.Server/Auth/IAuthenticator.cs`:

```csharp
using NATS.Server.Protocol;

namespace NATS.Server.Auth;

public interface IAuthenticator
{
    AuthResult? Authenticate(ClientAuthContext context);
}

public sealed class ClientAuthContext
{
    public required ClientOptions Opts { get; init; }
    public required byte[] Nonce { get; init; }
}
```

Create `src/NATS.Server/Auth/AuthResult.cs`:

```csharp
namespace NATS.Server.Auth;

public sealed class AuthResult
{
    public required string Identity { get; init; }
    public string? AccountName { get; init; }
    public Permissions? Permissions { get; init; }
    public DateTimeOffset? Expiry { get; init; }
}
```

Create `src/NATS.Server/Auth/TokenAuthenticator.cs`:

```csharp
using System.Security.Cryptography;
using System.Text;

namespace NATS.Server.Auth;

public sealed class TokenAuthenticator : IAuthenticator
{
    private readonly byte[] _expectedToken;

    public TokenAuthenticator(string token)
    {
        _expectedToken = Encoding.UTF8.GetBytes(token);
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var clientToken = context.Opts.Token;
        if (string.IsNullOrEmpty(clientToken))
            return null;

        var clientBytes = Encoding.UTF8.GetBytes(clientToken);

        if (!CryptographicOperations.FixedTimeEquals(clientBytes, _expectedToken))
            return null;

        return new AuthResult { Identity = "token" };
    }
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~TokenAuthenticatorTests" -v normal`
Expected: All 4 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/IAuthenticator.cs src/NATS.Server/Auth/AuthResult.cs src/NATS.Server/Auth/TokenAuthenticator.cs tests/NATS.Server.Tests/TokenAuthenticatorTests.cs
git commit -m "feat: add IAuthenticator interface and TokenAuthenticator with constant-time comparison"
```

---

### Task 5: Implement UserPasswordAuthenticator (plain + bcrypt)

**Files:**
- Create: `src/NATS.Server/Auth/UserPasswordAuthenticator.cs`
- Test: `tests/NATS.Server.Tests/UserPasswordAuthenticatorTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/UserPasswordAuthenticatorTests.cs`:

```csharp
using NATS.Server.Auth;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class UserPasswordAuthenticatorTests
{
    private static UserPasswordAuthenticator CreateAuth(params User[] users)
    {
        return new UserPasswordAuthenticator(users);
    }

    [Fact]
    public void Returns_result_for_correct_plain_password()
    {
        var auth = CreateAuth(new User { Username = "alice", Password = "secret" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "secret" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("alice");
    }

    [Fact]
    public void Returns_result_for_correct_bcrypt_password()
    {
        // Pre-computed bcrypt hash of "secret"
        var hash = BCrypt.Net.BCrypt.HashPassword("secret");
        var auth = CreateAuth(new User { Username = "bob", Password = hash });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "bob", Password = "secret" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("bob");
    }

    [Fact]
    public void Returns_null_for_wrong_password()
    {
        var auth = CreateAuth(new User { Username = "alice", Password = "secret" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "wrong" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_for_unknown_user()
    {
        var auth = CreateAuth(new User { Username = "alice", Password = "secret" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "unknown", Password = "secret" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_when_no_username_provided()
    {
        var auth = CreateAuth(new User { Username = "alice", Password = "secret" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions(),
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_permissions_from_user()
    {
        var perms = new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>"] },
        };
        var auth = CreateAuth(new User { Username = "alice", Password = "secret", Permissions = perms });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "secret" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Permissions.ShouldBe(perms);
    }

    [Fact]
    public void Returns_account_name_from_user()
    {
        var auth = CreateAuth(new User { Username = "alice", Password = "secret", Account = "myaccount" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "secret" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.AccountName.ShouldBe("myaccount");
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~UserPasswordAuthenticatorTests" -v normal`
Expected: FAIL — `UserPasswordAuthenticator` doesn't exist.

**Step 3: Implement UserPasswordAuthenticator**

Create `src/NATS.Server/Auth/UserPasswordAuthenticator.cs`:

```csharp
using System.Security.Cryptography;
using System.Text;

namespace NATS.Server.Auth;

public sealed class UserPasswordAuthenticator : IAuthenticator
{
    private readonly Dictionary<string, User> _users;

    public UserPasswordAuthenticator(IEnumerable<User> users)
    {
        _users = new Dictionary<string, User>(StringComparer.Ordinal);
        foreach (var user in users)
            _users[user.Username] = user;
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var username = context.Opts.Username;
        if (string.IsNullOrEmpty(username))
            return null;

        if (!_users.TryGetValue(username, out var user))
            return null;

        var clientPassword = context.Opts.Password ?? string.Empty;

        if (!ComparePasswords(user.Password, clientPassword))
            return null;

        return new AuthResult
        {
            Identity = user.Username,
            AccountName = user.Account,
            Permissions = user.Permissions,
            Expiry = user.ConnectionDeadline,
        };
    }

    private static bool ComparePasswords(string serverPassword, string clientPassword)
    {
        if (IsBcrypt(serverPassword))
        {
            try
            {
                return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword);
            }
            catch
            {
                return false;
            }
        }

        var serverBytes = Encoding.UTF8.GetBytes(serverPassword);
        var clientBytes = Encoding.UTF8.GetBytes(clientPassword);
        return CryptographicOperations.FixedTimeEquals(serverBytes, clientBytes);
    }

    private static bool IsBcrypt(string password) => password.StartsWith("$2");
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~UserPasswordAuthenticatorTests" -v normal`
Expected: All 7 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/UserPasswordAuthenticator.cs tests/NATS.Server.Tests/UserPasswordAuthenticatorTests.cs
git commit -m "feat: add UserPasswordAuthenticator with plain and bcrypt password support"
```

---

### Task 6: Implement SimpleUserPasswordAuthenticator

**Files:**
- Create: `src/NATS.Server/Auth/SimpleUserPasswordAuthenticator.cs`
- Test: `tests/NATS.Server.Tests/SimpleUserPasswordAuthenticatorTests.cs`

This handles the single `username`/`password` config option (no user map lookup).

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/SimpleUserPasswordAuthenticatorTests.cs`:

```csharp
using NATS.Server.Auth;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class SimpleUserPasswordAuthenticatorTests
{
    [Fact]
    public void Returns_result_for_correct_credentials()
    {
        var auth = new SimpleUserPasswordAuthenticator("admin", "password123");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "admin", Password = "password123" },
            Nonce = [],
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe("admin");
    }

    [Fact]
    public void Returns_null_for_wrong_username()
    {
        var auth = new SimpleUserPasswordAuthenticator("admin", "password123");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "wrong", Password = "password123" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_for_wrong_password()
    {
        var auth = new SimpleUserPasswordAuthenticator("admin", "password123");
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "admin", Password = "wrong" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Supports_bcrypt_password()
    {
        var hash = BCrypt.Net.BCrypt.HashPassword("secret");
        var auth = new SimpleUserPasswordAuthenticator("admin", hash);
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "admin", Password = "secret" },
            Nonce = [],
        };

        auth.Authenticate(ctx).ShouldNotBeNull();
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~SimpleUserPasswordAuthenticatorTests" -v normal`
Expected: FAIL — type doesn't exist.

**Step 3: Implement SimpleUserPasswordAuthenticator**

Create `src/NATS.Server/Auth/SimpleUserPasswordAuthenticator.cs`:

```csharp
using System.Security.Cryptography;
using System.Text;

namespace NATS.Server.Auth;

public sealed class SimpleUserPasswordAuthenticator : IAuthenticator
{
    private readonly byte[] _expectedUsername;
    private readonly string _serverPassword;

    public SimpleUserPasswordAuthenticator(string username, string password)
    {
        _expectedUsername = Encoding.UTF8.GetBytes(username);
        _serverPassword = password;
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var clientUsername = context.Opts.Username;
        if (string.IsNullOrEmpty(clientUsername))
            return null;

        var clientUsernameBytes = Encoding.UTF8.GetBytes(clientUsername);
        if (!CryptographicOperations.FixedTimeEquals(clientUsernameBytes, _expectedUsername))
            return null;

        var clientPassword = context.Opts.Password ?? string.Empty;

        if (!ComparePasswords(_serverPassword, clientPassword))
            return null;

        return new AuthResult { Identity = clientUsername };
    }

    private static bool ComparePasswords(string serverPassword, string clientPassword)
    {
        if (serverPassword.StartsWith("$2"))
        {
            try
            {
                return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword);
            }
            catch
            {
                return false;
            }
        }

        var serverBytes = Encoding.UTF8.GetBytes(serverPassword);
        var clientBytes = Encoding.UTF8.GetBytes(clientPassword);
        return CryptographicOperations.FixedTimeEquals(serverBytes, clientBytes);
    }
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~SimpleUserPasswordAuthenticatorTests" -v normal`
Expected: All 4 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/SimpleUserPasswordAuthenticator.cs tests/NATS.Server.Tests/SimpleUserPasswordAuthenticatorTests.cs
git commit -m "feat: add SimpleUserPasswordAuthenticator for single username/password config"
```

---

### Task 7: Implement NKeyAuthenticator

**Files:**
- Create: `src/NATS.Server/Auth/NKeyAuthenticator.cs`
- Test: `tests/NATS.Server.Tests/NKeyAuthenticatorTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/NKeyAuthenticatorTests.cs`:

```csharp
using NATS.NKeys;
using NATS.Server.Auth;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class NKeyAuthenticatorTests
{
    [Fact]
    public void Returns_result_for_valid_signature()
    {
        // Generate a test keypair
        var kp = KeyPair.CreateUser();
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce-123"u8.ToArray();
        var sig = kp.Sign(nonce);
        var sigBase64 = Convert.ToBase64URL(sig);

        var nkeyUser = new NKeyUser { Nkey = publicKey };
        var auth = new NKeyAuthenticator([nkeyUser]);

        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Nkey = publicKey, Sig = sigBase64 },
            Nonce = nonce,
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Identity.ShouldBe(publicKey);
    }

    [Fact]
    public void Returns_null_for_invalid_signature()
    {
        var kp = KeyPair.CreateUser();
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce-123"u8.ToArray();

        var nkeyUser = new NKeyUser { Nkey = publicKey };
        var auth = new NKeyAuthenticator([nkeyUser]);

        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Nkey = publicKey, Sig = Convert.ToBase64String(new byte[64]) },
            Nonce = nonce,
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_for_unknown_nkey()
    {
        var kp = KeyPair.CreateUser();
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce-123"u8.ToArray();
        var sig = kp.Sign(nonce);
        var sigBase64 = Convert.ToBase64URL(sig);

        // Server doesn't know about this key
        var auth = new NKeyAuthenticator([]);

        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Nkey = publicKey, Sig = sigBase64 },
            Nonce = nonce,
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_null_when_no_nkey_provided()
    {
        var kp = KeyPair.CreateUser();
        var publicKey = kp.GetPublicKey();
        var nkeyUser = new NKeyUser { Nkey = publicKey };
        var auth = new NKeyAuthenticator([nkeyUser]);

        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions(),
            Nonce = "nonce"u8.ToArray(),
        };

        auth.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Returns_permissions_from_nkey_user()
    {
        var kp = KeyPair.CreateUser();
        var publicKey = kp.GetPublicKey();
        var nonce = "test-nonce"u8.ToArray();
        var sig = kp.Sign(nonce);
        var sigBase64 = Convert.ToBase64URL(sig);

        var perms = new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>"] },
        };
        var nkeyUser = new NKeyUser { Nkey = publicKey, Permissions = perms };
        var auth = new NKeyAuthenticator([nkeyUser]);

        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Nkey = publicKey, Sig = sigBase64 },
            Nonce = nonce,
        };

        var result = auth.Authenticate(ctx);

        result.ShouldNotBeNull();
        result.Permissions.ShouldBe(perms);
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~NKeyAuthenticatorTests" -v normal`
Expected: FAIL — `NKeyAuthenticator` doesn't exist.

**Step 3: Implement NKeyAuthenticator**

Create `src/NATS.Server/Auth/NKeyAuthenticator.cs`:

```csharp
using NATS.NKeys;

namespace NATS.Server.Auth;

public sealed class NKeyAuthenticator : IAuthenticator
{
    private readonly Dictionary<string, NKeyUser> _nkeys;

    public NKeyAuthenticator(IEnumerable<NKeyUser> nkeyUsers)
    {
        _nkeys = new Dictionary<string, NKeyUser>(StringComparer.Ordinal);
        foreach (var nkeyUser in nkeyUsers)
            _nkeys[nkeyUser.Nkey] = nkeyUser;
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var clientNkey = context.Opts.Nkey;
        if (string.IsNullOrEmpty(clientNkey))
            return null;

        if (!_nkeys.TryGetValue(clientNkey, out var nkeyUser))
            return null;

        var clientSig = context.Opts.Sig;
        if (string.IsNullOrEmpty(clientSig))
            return null;

        try
        {
            var sigBytes = Convert.FromBase64String(
                clientSig.Replace('-', '+').Replace('_', '/').PadRight(
                    clientSig.Length + (4 - clientSig.Length % 4) % 4, '='));

            var kp = KeyPair.FromPublicKey(clientNkey);
            if (!kp.Verify(context.Nonce, sigBytes))
                return null;
        }
        catch
        {
            return null;
        }

        return new AuthResult
        {
            Identity = clientNkey,
            AccountName = nkeyUser.Account,
            Permissions = nkeyUser.Permissions,
        };
    }
}
```

> **Note for implementer:** The `Convert.FromBase64String` call with replacements handles both standard and URL-safe base64. The NATS client may send either encoding. If `Convert.ToBase64URL` is available in the test (it is in .NET 10), prefer that for the test side. The server must accept both.

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~NKeyAuthenticatorTests" -v normal`
Expected: All 5 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/NKeyAuthenticator.cs tests/NATS.Server.Tests/NKeyAuthenticatorTests.cs
git commit -m "feat: add NKeyAuthenticator with Ed25519 nonce signature verification"
```

---

### Task 8: Implement AuthService (orchestrator)

**Files:**
- Create: `src/NATS.Server/Auth/AuthService.cs`
- Test: `tests/NATS.Server.Tests/AuthServiceTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/AuthServiceTests.cs`:

```csharp
using NATS.Server.Auth;
using NATS.Server.Protocol;

namespace NATS.Server.Tests;

public class AuthServiceTests
{
    [Fact]
    public void IsAuthRequired_false_when_no_auth_configured()
    {
        var service = AuthService.Build(new NatsOptions());
        service.IsAuthRequired.ShouldBeFalse();
    }

    [Fact]
    public void IsAuthRequired_true_when_token_configured()
    {
        var service = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
        service.IsAuthRequired.ShouldBeTrue();
    }

    [Fact]
    public void IsAuthRequired_true_when_username_configured()
    {
        var service = AuthService.Build(new NatsOptions { Username = "admin", Password = "pass" });
        service.IsAuthRequired.ShouldBeTrue();
    }

    [Fact]
    public void IsAuthRequired_true_when_users_configured()
    {
        var opts = new NatsOptions
        {
            Users = [new User { Username = "alice", Password = "secret" }],
        };
        var service = AuthService.Build(opts);
        service.IsAuthRequired.ShouldBeTrue();
    }

    [Fact]
    public void IsAuthRequired_true_when_nkeys_configured()
    {
        var opts = new NatsOptions
        {
            NKeys = [new NKeyUser { Nkey = "UABC" }],
        };
        var service = AuthService.Build(opts);
        service.IsAuthRequired.ShouldBeTrue();
    }

    [Fact]
    public void Authenticate_returns_null_when_no_auth_and_creds_provided()
    {
        // No auth configured but client sends credentials — should still succeed (no auth required)
        var service = AuthService.Build(new NatsOptions());
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "anything" },
            Nonce = [],
        };

        // When auth is not required, Authenticate returns a default result
        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
    }

    [Fact]
    public void Authenticate_token_success()
    {
        var service = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "mytoken" },
            Nonce = [],
        };

        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
        result.Identity.ShouldBe("token");
    }

    [Fact]
    public void Authenticate_token_failure()
    {
        var service = AuthService.Build(new NatsOptions { Authorization = "mytoken" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Token = "wrong" },
            Nonce = [],
        };

        service.Authenticate(ctx).ShouldBeNull();
    }

    [Fact]
    public void Authenticate_simple_user_password_success()
    {
        var service = AuthService.Build(new NatsOptions { Username = "admin", Password = "pass" });
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "admin", Password = "pass" },
            Nonce = [],
        };

        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
        result.Identity.ShouldBe("admin");
    }

    [Fact]
    public void Authenticate_multi_user_success()
    {
        var opts = new NatsOptions
        {
            Users = [
                new User { Username = "alice", Password = "secret1" },
                new User { Username = "bob", Password = "secret2" },
            ],
        };
        var service = AuthService.Build(opts);
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "bob", Password = "secret2" },
            Nonce = [],
        };

        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
        result.Identity.ShouldBe("bob");
    }

    [Fact]
    public void NoAuthUser_fallback_when_no_creds()
    {
        var opts = new NatsOptions
        {
            Users = [
                new User { Username = "default", Password = "unused" },
            ],
            NoAuthUser = "default",
        };
        var service = AuthService.Build(opts);
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions(), // No credentials
            Nonce = [],
        };

        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
        result.Identity.ShouldBe("default");
    }

    [Fact]
    public void NKeys_tried_before_users()
    {
        // If both NKeys and Users are configured, NKeys should take priority
        var opts = new NatsOptions
        {
            NKeys = [new NKeyUser { Nkey = "UABC" }],
            Users = [new User { Username = "alice", Password = "secret" }],
        };
        var service = AuthService.Build(opts);

        // Provide user credentials — should still try NKey first (and fail for NKey),
        // then fall through to user auth
        var ctx = new ClientAuthContext
        {
            Opts = new ClientOptions { Username = "alice", Password = "secret" },
            Nonce = [],
        };

        var result = service.Authenticate(ctx);
        result.ShouldNotBeNull();
        result.Identity.ShouldBe("alice");
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthServiceTests" -v normal`
Expected: FAIL — `AuthService` doesn't exist.

**Step 3: Implement AuthService**

Create `src/NATS.Server/Auth/AuthService.cs`:

```csharp
using System.Security.Cryptography;

namespace NATS.Server.Auth;

public sealed class AuthService
{
    private readonly List<IAuthenticator> _authenticators;
    private readonly string? _noAuthUser;
    private readonly Dictionary<string, User>? _usersMap;

    public bool IsAuthRequired { get; }
    public bool NonceRequired { get; }

    private AuthService(List<IAuthenticator> authenticators, bool authRequired, bool nonceRequired,
        string? noAuthUser, Dictionary<string, User>? usersMap)
    {
        _authenticators = authenticators;
        IsAuthRequired = authRequired;
        NonceRequired = nonceRequired;
        _noAuthUser = noAuthUser;
        _usersMap = usersMap;
    }

    public static AuthService Build(NatsOptions options)
    {
        var authenticators = new List<IAuthenticator>();
        bool authRequired = false;
        bool nonceRequired = false;
        Dictionary<string, User>? usersMap = null;

        // Priority order (matching Go): NKeys > Users > Token > SimpleUserPassword

        if (options.NKeys is { Count: > 0 })
        {
            authenticators.Add(new NKeyAuthenticator(options.NKeys));
            authRequired = true;
            nonceRequired = true;
        }

        if (options.Users is { Count: > 0 })
        {
            authenticators.Add(new UserPasswordAuthenticator(options.Users));
            authRequired = true;
            usersMap = new Dictionary<string, User>(StringComparer.Ordinal);
            foreach (var u in options.Users)
                usersMap[u.Username] = u;
        }

        if (!string.IsNullOrEmpty(options.Authorization))
        {
            authenticators.Add(new TokenAuthenticator(options.Authorization));
            authRequired = true;
        }

        if (!string.IsNullOrEmpty(options.Username) && !string.IsNullOrEmpty(options.Password))
        {
            authenticators.Add(new SimpleUserPasswordAuthenticator(options.Username, options.Password));
            authRequired = true;
        }

        return new AuthService(authenticators, authRequired, nonceRequired, options.NoAuthUser, usersMap);
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        if (!IsAuthRequired)
            return new AuthResult { Identity = string.Empty };

        // Try each authenticator in priority order
        foreach (var authenticator in _authenticators)
        {
            var result = authenticator.Authenticate(context);
            if (result != null)
                return result;
        }

        // NoAuthUser fallback: if client provided no credentials and NoAuthUser is set
        if (_noAuthUser != null && IsNoCredentials(context))
        {
            return ResolveNoAuthUser();
        }

        return null;
    }

    private static bool IsNoCredentials(ClientAuthContext context)
    {
        var opts = context.Opts;
        return string.IsNullOrEmpty(opts.Username)
            && string.IsNullOrEmpty(opts.Password)
            && string.IsNullOrEmpty(opts.Token)
            && string.IsNullOrEmpty(opts.Nkey)
            && string.IsNullOrEmpty(opts.Sig);
    }

    private AuthResult? ResolveNoAuthUser()
    {
        if (_noAuthUser == null)
            return null;

        if (_usersMap != null && _usersMap.TryGetValue(_noAuthUser, out var user))
        {
            return new AuthResult
            {
                Identity = user.Username,
                AccountName = user.Account,
                Permissions = user.Permissions,
                Expiry = user.ConnectionDeadline,
            };
        }

        return new AuthResult { Identity = _noAuthUser };
    }

    public byte[] GenerateNonce()
    {
        Span<byte> raw = stackalloc byte[11];
        RandomNumberGenerator.Fill(raw);
        return raw.ToArray();
    }

    public string EncodeNonce(byte[] nonce)
    {
        return Convert.ToBase64String(nonce)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthServiceTests" -v normal`
Expected: All 12 tests PASS.

**Step 5: Run full test suite**

Run: `dotnet test`
Expected: All tests pass.

**Step 6: Commit**

```bash
git add src/NATS.Server/Auth/AuthService.cs tests/NATS.Server.Tests/AuthServiceTests.cs
git commit -m "feat: add AuthService orchestrator with priority-ordered authentication"
```

---

### Task 9: Implement ClientPermissions (publish/subscribe authorization)

**Files:**
- Create: `src/NATS.Server/Auth/ClientPermissions.cs`
- Test: `tests/NATS.Server.Tests/ClientPermissionsTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/ClientPermissionsTests.cs`:

```csharp
using NATS.Server.Auth;

namespace NATS.Server.Tests;

public class ClientPermissionsTests
{
    [Fact]
    public void No_permissions_allows_everything()
    {
        var perms = ClientPermissions.Build(null);

        perms.ShouldBeNull(); // null means no restrictions
    }

    [Fact]
    public void Publish_allow_list_only()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>", "bar.*"] },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        perms.IsPublishAllowed("foo.bar.baz").ShouldBeTrue();
        perms.IsPublishAllowed("bar.one").ShouldBeTrue();
        perms.IsPublishAllowed("baz.one").ShouldBeFalse();
    }

    [Fact]
    public void Publish_deny_list_only()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Deny = ["secret.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        perms.IsPublishAllowed("secret.data").ShouldBeFalse();
        perms.IsPublishAllowed("secret.nested.deep").ShouldBeFalse();
    }

    [Fact]
    public void Publish_allow_and_deny()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission
            {
                Allow = ["events.>"],
                Deny = ["events.internal.>"],
            },
        });

        perms.ShouldNotBeNull();
        perms.IsPublishAllowed("events.public.data").ShouldBeTrue();
        perms.IsPublishAllowed("events.internal.secret").ShouldBeFalse();
    }

    [Fact]
    public void Subscribe_allow_list()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission { Allow = ["data.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsSubscribeAllowed("data.updates").ShouldBeTrue();
        perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse();
    }

    [Fact]
    public void Subscribe_deny_list()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Subscribe = new SubjectPermission { Deny = ["admin.>"] },
        });

        perms.ShouldNotBeNull();
        perms.IsSubscribeAllowed("data.updates").ShouldBeTrue();
        perms.IsSubscribeAllowed("admin.logs").ShouldBeFalse();
    }

    [Fact]
    public void Publish_cache_returns_same_result()
    {
        var perms = ClientPermissions.Build(new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>"] },
        });

        perms.ShouldNotBeNull();

        // First call populates cache
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();
        // Second call should use cache — same result
        perms.IsPublishAllowed("foo.bar").ShouldBeTrue();

        perms.IsPublishAllowed("baz.bar").ShouldBeFalse();
        perms.IsPublishAllowed("baz.bar").ShouldBeFalse();
    }

    [Fact]
    public void Empty_permissions_object_allows_everything()
    {
        // Permissions object exists but has no publish or subscribe restrictions
        var perms = ClientPermissions.Build(new Permissions());

        perms.ShouldBeNull();
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClientPermissionsTests" -v normal`
Expected: FAIL — `ClientPermissions` doesn't exist.

**Step 3: Implement ClientPermissions**

Create `src/NATS.Server/Auth/ClientPermissions.cs`:

```csharp
using System.Collections.Concurrent;
using NATS.Server.Subscriptions;

namespace NATS.Server.Auth;

public sealed class ClientPermissions : IDisposable
{
    private readonly PermissionSet? _publish;
    private readonly PermissionSet? _subscribe;
    private readonly ConcurrentDictionary<string, bool> _pubCache = new(StringComparer.Ordinal);

    private ClientPermissions(PermissionSet? publish, PermissionSet? subscribe)
    {
        _publish = publish;
        _subscribe = subscribe;
    }

    public static ClientPermissions? Build(Permissions? permissions)
    {
        if (permissions == null)
            return null;

        var pub = PermissionSet.Build(permissions.Publish);
        var sub = PermissionSet.Build(permissions.Subscribe);

        if (pub == null && sub == null)
            return null;

        return new ClientPermissions(pub, sub);
    }

    public bool IsPublishAllowed(string subject)
    {
        if (_publish == null)
            return true;

        return _pubCache.GetOrAdd(subject, s => _publish.IsAllowed(s));
    }

    public bool IsSubscribeAllowed(string subject, string? queue = null)
    {
        if (_subscribe == null)
            return true;

        return _subscribe.IsAllowed(subject);
    }

    public void Dispose()
    {
        _publish?.Dispose();
        _subscribe?.Dispose();
    }
}

public sealed class PermissionSet : IDisposable
{
    private readonly SubList? _allow;
    private readonly SubList? _deny;

    private PermissionSet(SubList? allow, SubList? deny)
    {
        _allow = allow;
        _deny = deny;
    }

    public static PermissionSet? Build(SubjectPermission? permission)
    {
        if (permission == null)
            return null;

        bool hasAllow = permission.Allow is { Count: > 0 };
        bool hasDeny = permission.Deny is { Count: > 0 };

        if (!hasAllow && !hasDeny)
            return null;

        SubList? allow = null;
        SubList? deny = null;

        if (hasAllow)
        {
            allow = new SubList();
            foreach (var subject in permission.Allow!)
                allow.Insert(new Subscription { Subject = subject, Sid = "_perm_" });
        }

        if (hasDeny)
        {
            deny = new SubList();
            foreach (var subject in permission.Deny!)
                deny.Insert(new Subscription { Subject = subject, Sid = "_perm_" });
        }

        return new PermissionSet(allow, deny);
    }

    public bool IsAllowed(string subject)
    {
        bool allowed = true;

        // If allow list exists, subject must match it
        if (_allow != null)
        {
            var result = _allow.Match(subject);
            allowed = result.PlainSubs.Length > 0 || result.QueueSubs.Length > 0;
        }

        // If deny list exists, subject must NOT match it
        if (allowed && _deny != null)
        {
            var result = _deny.Match(subject);
            allowed = result.PlainSubs.Length == 0 && result.QueueSubs.Length == 0;
        }

        return allowed;
    }

    public void Dispose()
    {
        _allow?.Dispose();
        _deny?.Dispose();
    }
}
```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClientPermissionsTests" -v normal`
Expected: All 8 tests PASS.

**Step 5: Commit**

```bash
git add src/NATS.Server/Auth/ClientPermissions.cs tests/NATS.Server.Tests/ClientPermissionsTests.cs
git commit -m "feat: add ClientPermissions with SubList-based publish/subscribe authorization"
```

---

### Task 10: Integrate auth into NatsServer and NatsClient

This is the largest task — wire everything together. It modifies the server accept loop, client lifecycle, and message routing.

**Files:**
- Modify: `src/NATS.Server/NatsServer.cs` (add AuthService, accounts, per-client INFO)
- Modify: `src/NATS.Server/NatsClient.cs` (auth validation in ProcessConnect, permission checks in ProcessSub/ProcessPub, auth timeout, account assignment)

**Step 1: Write failing integration test**

Create `tests/NATS.Server.Tests/AuthIntegrationTests.cs`:

```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server;
using NATS.Server.Auth;

namespace NATS.Server.Tests;

public class AuthIntegrationTests : IAsyncLifetime
{
    private NatsServer? _server;
    private int _port;
    private CancellationTokenSource _cts = new();
    private Task _serverTask = null!;

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    private void StartServer(NatsOptions opts)
    {
        _port = GetFreePort();
        opts.Port = _port;
        _server = new NatsServer(opts, NullLoggerFactory.Instance);
    }

    public async Task InitializeAsync()
    {
        // Default server — tests will create their own via StartServer
    }

    public async Task DisposeAsync()
    {
        await _cts.CancelAsync();
        _server?.Dispose();
    }

    private async Task StartAndWaitAsync()
    {
        _serverTask = _server!.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    private NatsConnection CreateClient(string? user = null, string? pass = null, string? token = null)
    {
        var url = $"nats://";
        if (user != null && pass != null)
            url = $"nats://{user}:{pass}@";
        else if (token != null)
            url = $"nats://token@"; // NATS client uses this format for token

        var opts = new NatsOpts
        {
            Url = $"{url}127.0.0.1:{_port}",
            AuthOpts = token != null ? new NatsAuthOpts { Token = token } : default,
        };
        return new NatsConnection(opts);
    }

    [Fact]
    public async Task Token_auth_success()
    {
        StartServer(new NatsOptions { Authorization = "mytoken" });
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
            AuthOpts = new NatsAuthOpts { Token = "mytoken" },
        });

        await client.ConnectAsync();
        await client.PingAsync();
    }

    [Fact]
    public async Task Token_auth_failure_disconnects()
    {
        StartServer(new NatsOptions { Authorization = "mytoken" });
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
            AuthOpts = new NatsAuthOpts { Token = "wrong" },
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });
    }

    [Fact]
    public async Task UserPassword_auth_success()
    {
        StartServer(new NatsOptions { Username = "admin", Password = "secret" });
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://admin:secret@127.0.0.1:{_port}",
        });

        await client.ConnectAsync();
        await client.PingAsync();
    }

    [Fact]
    public async Task UserPassword_auth_failure_disconnects()
    {
        StartServer(new NatsOptions { Username = "admin", Password = "secret" });
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://admin:wrong@127.0.0.1:{_port}",
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });
    }

    [Fact]
    public async Task MultiUser_auth_success()
    {
        StartServer(new NatsOptions
        {
            Users = [
                new User { Username = "alice", Password = "pass1" },
                new User { Username = "bob", Password = "pass2" },
            ],
        });
        await StartAndWaitAsync();

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

    [Fact]
    public async Task No_credentials_when_auth_required_disconnects()
    {
        StartServer(new NatsOptions { Authorization = "mytoken" });
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });
    }

    [Fact]
    public async Task No_auth_configured_allows_all()
    {
        StartServer(new NatsOptions());
        await StartAndWaitAsync();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
        });

        await client.ConnectAsync();
        await client.PingAsync();
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthIntegrationTests" -v normal`
Expected: FAIL — auth tests fail because server doesn't enforce authentication.

**Step 3: Modify NatsServer to integrate AuthService**

In `src/NATS.Server/NatsServer.cs`, apply these changes:

1. Add field: `private readonly AuthService _authService;`
2. Add field: `private readonly ConcurrentDictionary<string, Account> _accounts = new();`
3. Add field: `private Account _globalAccount;`
4. In constructor, after `_serverInfo` creation:
   ```csharp
   _authService = AuthService.Build(options);
   _globalAccount = new Account(Account.GlobalAccountName);
   _accounts[_globalAccount.Name] = _globalAccount;
   ```
5. In accept loop, when creating client — create per-client `ServerInfo` with nonce:
   ```csharp
   var clientInfo = CreateClientInfo(clientId, socket);
   var client = new NatsClient(clientId, socket, _options, clientInfo, _authService, this, clientLogger);
   ```
6. Add method `CreateClientInfo`:
   ```csharp
   private ServerInfo CreateClientInfo(ulong clientId, Socket socket)
   {
       var info = new ServerInfo
       {
           ServerId = _serverInfo.ServerId,
           ServerName = _serverInfo.ServerName,
           Version = _serverInfo.Version,
           Host = _serverInfo.Host,
           Port = _serverInfo.Port,
           MaxPayload = _serverInfo.MaxPayload,
           ClientId = clientId,
           ClientIp = (socket.RemoteEndPoint as IPEndPoint)?.Address.ToString(),
           AuthRequired = _authService.IsAuthRequired,
       };

       if (_authService.NonceRequired)
       {
           var nonce = _authService.GenerateNonce();
           info.Nonce = _authService.EncodeNonce(nonce);
           // Store raw nonce bytes for signature verification — passed to NatsClient
       }

       return info;
   }
   ```
7. Change `SubList` property to use the request's account SubList or the global account SubList.
8. Update `ProcessMessage` to use the sender's account's SubList instead of `_subList`. The server-level `_subList` field is replaced by per-account SubLists.

**Step 4: Modify NatsClient to enforce authentication**

In `src/NATS.Server/NatsClient.cs`, apply these changes:

1. Add fields:
   ```csharp
   private readonly AuthService _authService;
   private Account? _account;
   private ClientPermissions? _permissions;
   private byte[]? _nonce;
   ```
2. Update constructor to accept `AuthService` and store nonce.
3. In `RunAsync`, after sending INFO, add auth timeout:
   ```csharp
   if (_authService.IsAuthRequired)
   {
       using var authTimeout = new CancellationTokenSource(_options.AuthTimeout);
       using var combined = CancellationTokenSource.CreateLinkedTokenSource(
           _clientCts.Token, authTimeout.Token);
       // Wait for CONNECT to be processed with auth
       // ...
   }
   ```
4. In `ProcessConnect`, after deserializing `ClientOpts`, call auth:
   ```csharp
   if (_authService.IsAuthRequired)
   {
       var ctx = new ClientAuthContext
       {
           Opts = ClientOpts,
           Nonce = _nonce ?? [],
       };
       var result = _authService.Authenticate(ctx);
       if (result == null)
       {
           await SendErrAndCloseAsync(NatsProtocol.ErrAuthorizationViolation);
           return;
       }
       _permissions = ClientPermissions.Build(result.Permissions);
       // Assign account
   }
   ```
5. In `ProcessSub`, add permission check before insert:
   ```csharp
   if (_permissions != null && !_permissions.IsSubscribeAllowed(cmd.Subject!, cmd.Queue))
   {
       await SendErrAsync($"{NatsProtocol.ErrPermissionsSubscribe} to \"{cmd.Subject}\"");
       return;
   }
   ```
6. In `ProcessPubAsync`, add permission check before routing:
   ```csharp
   if (_permissions != null && !_permissions.IsPublishAllowed(cmd.Subject!))
   {
       await SendErrAsync($"{NatsProtocol.ErrPermissionsPublish} to \"{cmd.Subject}\"");
       return;
   }
   ```

> **Note for implementer:** This is the most complex integration task. The exact modifications depend on how the auth timeout interleaving works. The key principle is: ProcessConnect becomes async (to send -ERR), returns a success/failure signal, and RunAsync gates all further command processing on auth success. One clean approach is to add a `TaskCompletionSource<bool> _authCompleted` that ProcessConnect sets. RunAsync waits on it (with timeout) before starting the main command loop.

**Step 5: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AuthIntegrationTests" -v normal`
Expected: All 7 tests PASS.

**Step 6: Run full test suite to check for regressions**

Run: `dotnet test`
Expected: All tests pass. Existing tests that don't use auth should still work because when no auth is configured, `AuthService.IsAuthRequired` is `false` and auth is bypassed.

**Step 7: Commit**

```bash
git add src/NATS.Server/NatsServer.cs src/NATS.Server/NatsClient.cs tests/NATS.Server.Tests/AuthIntegrationTests.cs
git commit -m "feat: integrate authentication into server accept loop and client CONNECT processing"
```

---

### Task 11: Implement account isolation in message routing

**Files:**
- Modify: `src/NATS.Server/NatsServer.cs` (ProcessMessage uses account SubList)
- Modify: `src/NATS.Server/NatsClient.cs` (subscriptions go to account SubList)
- Test: `tests/NATS.Server.Tests/AccountIsolationTests.cs`

**Step 1: Write failing test**

Create `tests/NATS.Server.Tests/AccountIsolationTests.cs`:

```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server;
using NATS.Server.Auth;

namespace NATS.Server.Tests;

public class AccountIsolationTests : IAsyncLifetime
{
    private NatsServer _server = null!;
    private int _port;
    private readonly CancellationTokenSource _cts = new();
    private Task _serverTask = null!;

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    public async Task InitializeAsync()
    {
        _port = GetFreePort();
        _server = new NatsServer(new NatsOptions
        {
            Port = _port,
            Users =
            [
                new User { Username = "alice", Password = "pass", Account = "acct-a" },
                new User { Username = "bob", Password = "pass", Account = "acct-b" },
                new User { Username = "charlie", Password = "pass", Account = "acct-a" },
            ],
        }, NullLoggerFactory.Instance);

        _serverTask = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        await _cts.CancelAsync();
        _server.Dispose();
    }

    [Fact]
    public async Task Same_account_receives_messages()
    {
        // Alice and Charlie are in acct-a
        await using var alice = new NatsConnection(new NatsOpts
        {
            Url = $"nats://alice:pass@127.0.0.1:{_port}",
        });
        await using var charlie = new NatsConnection(new NatsOpts
        {
            Url = $"nats://charlie:pass@127.0.0.1:{_port}",
        });

        await alice.ConnectAsync();
        await charlie.ConnectAsync();

        await using var sub = await charlie.SubscribeCoreAsync<string>("test.subject");
        await charlie.PingAsync();

        await alice.PublishAsync("test.subject", "from-alice");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("from-alice");
    }

    [Fact]
    public async Task Different_account_does_not_receive_messages()
    {
        // Alice is in acct-a, Bob is in acct-b
        await using var alice = new NatsConnection(new NatsOpts
        {
            Url = $"nats://alice:pass@127.0.0.1:{_port}",
        });
        await using var bob = new NatsConnection(new NatsOpts
        {
            Url = $"nats://bob:pass@127.0.0.1:{_port}",
        });

        await alice.ConnectAsync();
        await bob.ConnectAsync();

        await using var sub = await bob.SubscribeCoreAsync<string>("test.subject");
        await bob.PingAsync();

        await alice.PublishAsync("test.subject", "from-alice");

        // Bob should NOT receive this — wait briefly then verify nothing arrived
        using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
        try
        {
            await sub.Msgs.ReadAsync(timeout.Token);
            throw new Exception("Bob should not have received a message from a different account");
        }
        catch (OperationCanceledException)
        {
            // Expected — no message received (timeout)
        }
    }
}
```

**Step 2: Run test to verify it fails**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AccountIsolationTests" -v normal`
Expected: FAIL — account isolation not implemented.

**Step 3: Implement account-aware routing**

The key changes (building on Task 10):

1. In `NatsServer`, add account resolution:
   ```csharp
   public Account GetOrCreateAccount(string? name)
   {
       if (string.IsNullOrEmpty(name))
           return _globalAccount;
       return _accounts.GetOrAdd(name, n => new Account(n));
   }
   ```

2. In `NatsClient`, after auth success, resolve account:
   ```csharp
   _account = _server.GetOrCreateAccount(result.AccountName);
   _account.AddClient(Id);
   ```

3. In `NatsClient.ProcessSub`, insert subscription into account's SubList:
   ```csharp
   _account?.SubList.Insert(sub);
   ```

4. In `NatsServer.ProcessMessage`, use the sender's account SubList:
   ```csharp
   public void ProcessMessage(string subject, string? replyTo, ReadOnlyMemory<byte> headers,
       ReadOnlyMemory<byte> payload, NatsClient sender)
   {
       var subList = sender.Account?.SubList ?? _globalAccount.SubList;
       var result = subList.Match(subject);
       // ... rest of delivery logic unchanged
   }
   ```

5. In cleanup (`RemoveClient`), remove from account:
   ```csharp
   client.Account?.RemoveClient(client.Id);
   client.RemoveAllSubscriptions(client.Account?.SubList ?? _globalAccount.SubList);
   ```

**Step 4: Run tests to verify they pass**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~AccountIsolationTests" -v normal`
Expected: Both tests PASS.

**Step 5: Run full test suite**

Run: `dotnet test`
Expected: All tests pass.

**Step 6: Commit**

```bash
git add src/NATS.Server/NatsServer.cs src/NATS.Server/NatsClient.cs tests/NATS.Server.Tests/AccountIsolationTests.cs
git commit -m "feat: add per-account SubList isolation for message routing"
```

---

### Task 12: Add permission enforcement integration tests

**Files:**
- Test: `tests/NATS.Server.Tests/PermissionIntegrationTests.cs`

**Step 1: Write permission integration tests**

Create `tests/NATS.Server.Tests/PermissionIntegrationTests.cs`:

```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server;
using NATS.Server.Auth;

namespace NATS.Server.Tests;

public class PermissionIntegrationTests : IAsyncLifetime
{
    private NatsServer _server = null!;
    private int _port;
    private readonly CancellationTokenSource _cts = new();
    private Task _serverTask = null!;

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    public async Task InitializeAsync()
    {
        _port = GetFreePort();
        _server = new NatsServer(new NatsOptions
        {
            Port = _port,
            Users =
            [
                new User
                {
                    Username = "publisher",
                    Password = "pass",
                    Permissions = new Permissions
                    {
                        Publish = new SubjectPermission { Allow = ["events.>"] },
                        Subscribe = new SubjectPermission { Deny = [">"] },
                    },
                },
                new User
                {
                    Username = "subscriber",
                    Password = "pass",
                    Permissions = new Permissions
                    {
                        Publish = new SubjectPermission { Deny = [">"] },
                        Subscribe = new SubjectPermission { Allow = ["events.>"] },
                    },
                },
                new User
                {
                    Username = "admin",
                    Password = "pass",
                    // No permissions — full access
                },
            ],
        }, NullLoggerFactory.Instance);

        _serverTask = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        await _cts.CancelAsync();
        _server.Dispose();
    }

    [Fact]
    public async Task Publisher_can_publish_to_allowed_subject()
    {
        await using var pub = new NatsConnection(new NatsOpts
        {
            Url = $"nats://publisher:pass@127.0.0.1:{_port}",
        });
        await using var admin = new NatsConnection(new NatsOpts
        {
            Url = $"nats://admin:pass@127.0.0.1:{_port}",
        });

        await pub.ConnectAsync();
        await admin.ConnectAsync();

        await using var sub = await admin.SubscribeCoreAsync<string>("events.test");
        await admin.PingAsync();

        await pub.PublishAsync("events.test", "hello");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("hello");
    }

    [Fact]
    public async Task Admin_has_full_access()
    {
        await using var admin1 = new NatsConnection(new NatsOpts
        {
            Url = $"nats://admin:pass@127.0.0.1:{_port}",
        });
        await using var admin2 = new NatsConnection(new NatsOpts
        {
            Url = $"nats://admin:pass@127.0.0.1:{_port}",
        });

        await admin1.ConnectAsync();
        await admin2.ConnectAsync();

        await using var sub = await admin2.SubscribeCoreAsync<string>("anything.at.all");
        await admin2.PingAsync();

        await admin1.PublishAsync("anything.at.all", "data");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await sub.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("data");
    }
}
```

**Step 2: Run tests**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~PermissionIntegrationTests" -v normal`
Expected: All tests PASS (permissions were integrated in Task 10).

**Step 3: Commit**

```bash
git add tests/NATS.Server.Tests/PermissionIntegrationTests.cs
git commit -m "test: add permission enforcement integration tests"
```

---

### Task 13: Add NKey integration test

**Files:**
- Test: `tests/NATS.Server.Tests/NKeyIntegrationTests.cs`

**Step 1: Write NKey integration test**

Create `tests/NATS.Server.Tests/NKeyIntegrationTests.cs`:

```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.NKeys;
using NATS.Server;
using NATS.Server.Auth;

namespace NATS.Server.Tests;

public class NKeyIntegrationTests : IAsyncLifetime
{
    private NatsServer _server = null!;
    private int _port;
    private readonly CancellationTokenSource _cts = new();
    private Task _serverTask = null!;
    private KeyPair _userKeyPair = null!;
    private string _userSeed = null!;
    private string _userPublicKey = null!;

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    public async Task InitializeAsync()
    {
        _port = GetFreePort();
        _userKeyPair = KeyPair.CreateUser();
        _userPublicKey = _userKeyPair.GetPublicKey();
        _userSeed = _userKeyPair.GetSeed();

        _server = new NatsServer(new NatsOptions
        {
            Port = _port,
            NKeys = [new NKeyUser { Nkey = _userPublicKey }],
        }, NullLoggerFactory.Instance);

        _serverTask = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        await _cts.CancelAsync();
        _server.Dispose();
    }

    [Fact]
    public async Task NKey_auth_success()
    {
        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
            AuthOpts = new NatsAuthOpts { NKey = _userPublicKey, Seed = _userSeed },
        });

        await client.ConnectAsync();
        await client.PingAsync();
    }

    [Fact]
    public async Task NKey_auth_wrong_key_fails()
    {
        // Generate a different key pair not known to the server
        var otherKp = KeyPair.CreateUser();

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = $"nats://127.0.0.1:{_port}",
            AuthOpts = new NatsAuthOpts { NKey = otherKp.GetPublicKey(), Seed = otherKp.GetSeed() },
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await client.ConnectAsync();
            await client.PingAsync();
        });
    }
}
```

> **Note for implementer:** The NATS.Client.Core library handles NKey signing automatically when you provide the NKey and Seed in `NatsAuthOpts`. It reads the server's nonce from INFO, signs it with the seed, and sends the public key + signature in CONNECT. Verify that `NATS.Client.Core` 2.7.2 supports `NatsAuthOpts.NKey` and `NatsAuthOpts.Seed` — check the NuGet docs if needed.

**Step 2: Run tests**

Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~NKeyIntegrationTests" -v normal`
Expected: Both tests PASS.

**Step 3: Commit**

```bash
git add tests/NATS.Server.Tests/NKeyIntegrationTests.cs
git commit -m "test: add NKey authentication integration tests"
```

---

### Task 14: Final regression test and cleanup

**Files:**
- No new files — run all tests and verify

**Step 1: Run full test suite**

Run: `dotnet test -v normal`
Expected: ALL tests pass — both old (parser, sublist, subject match, client, server, integration) and new (auth protocol, auth config, account, token auth, user/pass auth, simple user/pass auth, nkey auth, auth service, client permissions, auth integration, account isolation, permission integration, nkey integration).

**Step 2: Build with no warnings**

Run: `dotnet build -warnaserror`
Expected: Build succeeds with zero warnings.

**Step 3: Commit (if any cleanup needed)**

```bash
git add -A
git commit -m "chore: final cleanup for authentication implementation"
```

---

## File Summary

### New Files (13)
| File | Purpose |
|------|---------|
| `src/NATS.Server/Auth/User.cs` | Username/password user model |
| `src/NATS.Server/Auth/NKeyUser.cs` | NKey user model |
| `src/NATS.Server/Auth/Permissions.cs` | Permission types (SubjectPermission, ResponsePermission) |
| `src/NATS.Server/Auth/Account.cs` | Per-account SubList and client tracking |
| `src/NATS.Server/Auth/IAuthenticator.cs` | Auth interface and ClientAuthContext |
| `src/NATS.Server/Auth/AuthResult.cs` | Authentication result |
| `src/NATS.Server/Auth/TokenAuthenticator.cs` | Token-based auth |
| `src/NATS.Server/Auth/UserPasswordAuthenticator.cs` | Multi-user password auth |
| `src/NATS.Server/Auth/SimpleUserPasswordAuthenticator.cs` | Single user/pass auth |
| `src/NATS.Server/Auth/NKeyAuthenticator.cs` | NKey Ed25519 auth |
| `src/NATS.Server/Auth/AuthService.cs` | Auth orchestrator |
| `src/NATS.Server/Auth/ClientPermissions.cs` | Publish/subscribe permission enforcement |

### Modified Files (5)
| File | Changes |
|------|---------|
| `Directory.Packages.props` | Add NATS.NKeys, BCrypt.Net-Next |
| `src/NATS.Server/NATS.Server.csproj` | Add package references |
| `src/NATS.Server/Protocol/NatsProtocol.cs` | Auth errors, ServerInfo.AuthRequired/Nonce, ClientOptions auth fields |
| `src/NATS.Server/NatsOptions.cs` | Auth configuration fields |
| `src/NATS.Server/NatsServer.cs` | AuthService integration, account management, per-client INFO |
| `src/NATS.Server/NatsClient.cs` | Auth validation, permission checks, account assignment |

### New Test Files (9)
| File | Tests |
|------|-------|
| `tests/NATS.Server.Tests/AuthProtocolTests.cs` | Protocol type serialization |
| `tests/NATS.Server.Tests/AuthConfigTests.cs` | Options defaults |
| `tests/NATS.Server.Tests/AccountTests.cs` | Account model |
| `tests/NATS.Server.Tests/TokenAuthenticatorTests.cs` | Token auth |
| `tests/NATS.Server.Tests/UserPasswordAuthenticatorTests.cs` | User/pass auth |
| `tests/NATS.Server.Tests/SimpleUserPasswordAuthenticatorTests.cs` | Simple user/pass auth |
| `tests/NATS.Server.Tests/NKeyAuthenticatorTests.cs` | NKey auth |
| `tests/NATS.Server.Tests/AuthServiceTests.cs` | Auth orchestration |
| `tests/NATS.Server.Tests/ClientPermissionsTests.cs` | Permission checks |
| `tests/NATS.Server.Tests/AuthIntegrationTests.cs` | Full auth integration |
| `tests/NATS.Server.Tests/AccountIsolationTests.cs` | Cross-account isolation |
| `tests/NATS.Server.Tests/PermissionIntegrationTests.cs` | Permission enforcement |
| `tests/NATS.Server.Tests/NKeyIntegrationTests.cs` | NKey end-to-end |
