# Authentication Design **Date:** 2026-02-22 **Scope:** Username/password, token, NKeys, permissions, core account isolation **Approach:** Strategy pattern with pluggable authenticators ## Overview Port the NATS server authentication subsystem from Go (`server/auth.go`, `server/accounts.go`) to .NET. The system authenticates clients during the CONNECT phase using nonce-based challenge-response (NKeys), password comparison (plain/bcrypt), or bearer tokens. Each authenticated user can have per-subject publish/subscribe permissions. Clients are assigned to accounts that provide subscription isolation. ## Core Auth Models ### User (username/password auth) ```csharp public sealed class User { public required string Username { get; init; } public required string Password { get; init; } // plain or bcrypt ($2...) public Permissions? Permissions { get; init; } public Account? Account { get; init; } public DateTimeOffset? ConnectionDeadline { get; init; } } ``` ### NKeyUser (Ed25519 auth) ```csharp public sealed class NKeyUser { public required string Nkey { get; init; } // public nkey public Permissions? Permissions { get; init; } public Account? Account { get; init; } public string? SigningKey { get; init; } } ``` ### Permissions ```csharp 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? Allow { get; init; } public IReadOnlyList? Deny { get; init; } } public sealed class ResponsePermission { public int MaxMsgs { get; init; } public TimeSpan Expires { get; init; } } ``` ### Account Per-account isolation with its own SubList: ```csharp public sealed class Account { public string Name { get; } public SubList SubList { get; } public ConcurrentDictionary Clients { get; } public Permissions? DefaultPermissions { get; set; } } ``` Server holds `ConcurrentDictionary` with lazy global account (`DEFAULT`). ## Authentication Flow ``` Client connects -> Server sends INFO { auth_required: true, nonce: "..." } -> Client sends CONNECT { user, pass, token, nkey, sig } -> AuthService.Authenticate(context) |- NKeyAuthenticator (if nkeys configured) |- UserPasswordAuthenticator (if users configured) |- TokenAuthenticator (if token configured) |- SimpleUserPasswordAuthenticator (if single user configured) '- NoAuthUser fallback (if configured) -> Success: assign permissions, assign account -> Failure: -ERR 'Authorization Violation', disconnect ``` ### Interfaces ```csharp public interface IAuthenticator { AuthResult? Authenticate(ClientAuthContext context); } public sealed class ClientAuthContext { public required ClientOptions Opts { get; init; } public required byte[] Nonce { get; init; } public required NatsClient Client { get; init; } } public sealed class AuthResult { public required string Identity { get; init; } public Account Account { get; init; } public Permissions? Permissions { get; init; } public DateTimeOffset? Expiry { get; init; } } ``` ### AuthService Orchestrates authenticators in priority order (matching Go's `processClientOrLeafAuthentication`): ```csharp public sealed class AuthService { private readonly List _authenticators; public AuthResult? Authenticate(ClientAuthContext context); public bool IsAuthRequired { get; } } ``` ### Concrete Authenticators - **NKeyAuthenticator** - looks up nkey in server map, verifies Ed25519 signature against nonce using NATS.NKeys - **UserPasswordAuthenticator** - looks up username in server map, compares password (bcrypt via BCrypt.Net-Next, or constant-time plain comparison) - **TokenAuthenticator** - constant-time comparison against configured token - **SimpleUserPasswordAuthenticator** - for single `username`/`password` option (no map lookup) ### Auth Timeout `CancellationTokenSource` with configurable timeout (default 1 second, matching Go `DEFAULT_AUTH_TIMEOUT`). If CONNECT not received and authenticated within window, disconnect client. ### Nonce Generation Per-client nonce: 11 random bytes, base64url-encoded to 15 chars (matching Go). Generated using `RandomNumberGenerator.Fill()`. Included in INFO only when auth is required. ### Password Comparison - Bcrypt passwords (prefixed `$2`): use `BCrypt.Net.BCrypt.Verify()` (constant-time) - Plain passwords: use `CryptographicOperations.FixedTimeEquals()` (constant-time) ## Permission Enforcement ### ClientPermissions ```csharp public sealed class ClientPermissions { public PermissionSet? Publish { get; init; } public PermissionSet? Subscribe { get; init; } public ResponsePermission? Response { get; init; } private readonly ConcurrentDictionary _pubCache = new(); public bool IsPublishAllowed(string subject); public bool IsSubscribeAllowed(string subject, string? queue = null); } public sealed class PermissionSet { public SubList? Allow { get; init; } public SubList? Deny { get; init; } } ``` ### Logic (matches Go) 1. No permissions set -> allow everything 2. Allow list exists -> subject must match allow list 3. Deny list exists -> subject must NOT match deny list 4. Subscribe checks queue groups against queue-specific entries ### Integration Points - `ProcessPub()` - check `IsPublishAllowed(subject)` before routing. `-ERR 'Permissions Violation for Publish to ""'` on deny. - `ProcessSub()` - check `IsSubscribeAllowed(subject, queue)` before adding subscription. `-ERR 'Permissions Violation for Subscription to ""'` on deny. ## Configuration Changes ### NatsOptions ```csharp public sealed class NatsOptions { // ... existing fields ... // Simple auth (single user) public string? Username { get; set; } public string? Password { get; set; } public string? Authorization { get; set; } // bearer token // Multiple users/nkeys public IReadOnlyList? Users { get; set; } public IReadOnlyList? NKeys { get; set; } // Default/fallback public string? NoAuthUser { get; set; } // Auth timing public TimeSpan AuthTimeout { get; set; } = TimeSpan.FromSeconds(1); } ``` ### ServerInfo Additions ```csharp public bool AuthRequired { get; set; } public string? Nonce { get; set; } ``` ### ClientOptions Additions ```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; } ``` ## Server/Client Integration ### NatsServer - Build `AuthService` during startup based on options - Maintain `ConcurrentDictionary` with lazy global account - Build `users` and `nkeys` lookup maps from options - Generate per-client nonce when `AuthService.IsAuthRequired` - Include `AuthRequired` and `Nonce` in per-client INFO ### NatsClient - Store `ClientPermissions` after successful auth - Store assigned `Account` reference - Auth timeout via `CancellationTokenSource` on CONNECT wait - Register/unregister with account's client set on connect/disconnect - PUB messages route through account's SubList for isolation ## NuGet Packages - `NATS.NKeys` - Ed25519 NKey operations - `BCrypt.Net-Next` - bcrypt password hashing ## Testing Strategy ### Unit Tests - `TokenAuthenticatorTests` - correct/wrong token, constant-time comparison - `UserPasswordAuthenticatorTests` - plain password, bcrypt password, wrong password, unknown user - `NKeyAuthenticatorTests` - valid/invalid signature, unknown nkey - `AuthServiceTests` - priority ordering, NoAuthUser fallback, auth-not-required case - `ClientPermissionsTests` - allow/deny lists, wildcards, queue groups, publish cache ### Account Tests - Two accounts configured, cross-account isolation verified ### Integration Tests - Connect with valid token/user-pass/nkey -> success - Connect with wrong credentials -> `-ERR`, disconnect - Connect with no credentials when auth required -> `-ERR`, disconnect - NoAuthUser configured -> success with default permissions - Auth timeout -> disconnect - Publish/subscribe to denied subjects -> `-ERR 'Permissions Violation ...'` ## Go Reference Files - `server/auth.go` - Core authentication logic - `server/accounts.go` - Account struct and management - `server/client.go` - CONNECT processing, RegisterUser, RegisterNkeyUser, permission checks - `server/nkey.go` - Nonce generation - `server/opts.go` - Auth configuration parsing