From bca703b31019bbccaf664186ee376517f6c64ac9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 22:07:16 -0500 Subject: [PATCH] docs: add authentication design for username/password, token, and NKeys Covers auth models, strategy pattern with pluggable authenticators, permission enforcement, core account isolation, and server integration. --- .../plans/2026-02-22-authentication-design.md | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 docs/plans/2026-02-22-authentication-design.md diff --git a/docs/plans/2026-02-22-authentication-design.md b/docs/plans/2026-02-22-authentication-design.md new file mode 100644 index 0000000..418e99d --- /dev/null +++ b/docs/plans/2026-02-22-authentication-design.md @@ -0,0 +1,286 @@ +# 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