Covers auth models, strategy pattern with pluggable authenticators, permission enforcement, core account isolation, and server integration.
8.7 KiB
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)
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)
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
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; }
}
Account
Per-account isolation with its own SubList:
public sealed class Account
{
public string Name { get; }
public SubList SubList { get; }
public ConcurrentDictionary<ulong, NatsClient> Clients { get; }
public Permissions? DefaultPermissions { get; set; }
}
Server holds ConcurrentDictionary<string, Account> 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
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):
public sealed class AuthService
{
private readonly List<IAuthenticator> _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/passwordoption (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): useBCrypt.Net.BCrypt.Verify()(constant-time) - Plain passwords: use
CryptographicOperations.FixedTimeEquals()(constant-time)
Permission Enforcement
ClientPermissions
public sealed class ClientPermissions
{
public PermissionSet? Publish { get; init; }
public PermissionSet? Subscribe { get; init; }
public ResponsePermission? Response { get; init; }
private readonly ConcurrentDictionary<string, bool> _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)
- No permissions set -> allow everything
- Allow list exists -> subject must match allow list
- Deny list exists -> subject must NOT match deny list
- Subscribe checks queue groups against queue-specific entries
Integration Points
ProcessPub()- checkIsPublishAllowed(subject)before routing.-ERR 'Permissions Violation for Publish to "<subject>"'on deny.ProcessSub()- checkIsSubscribeAllowed(subject, queue)before adding subscription.-ERR 'Permissions Violation for Subscription to "<subject>"'on deny.
Configuration Changes
NatsOptions
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<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);
}
ServerInfo Additions
public bool AuthRequired { get; set; }
public string? Nonce { get; set; }
ClientOptions Additions
[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
AuthServiceduring startup based on options - Maintain
ConcurrentDictionary<string, Account>with lazy global account - Build
usersandnkeyslookup maps from options - Generate per-client nonce when
AuthService.IsAuthRequired - Include
AuthRequiredandNoncein per-client INFO
NatsClient
- Store
ClientPermissionsafter successful auth - Store assigned
Accountreference - Auth timeout via
CancellationTokenSourceon 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 operationsBCrypt.Net-Next- bcrypt password hashing
Testing Strategy
Unit Tests
TokenAuthenticatorTests- correct/wrong token, constant-time comparisonUserPasswordAuthenticatorTests- plain password, bcrypt password, wrong password, unknown userNKeyAuthenticatorTests- valid/invalid signature, unknown nkeyAuthServiceTests- priority ordering, NoAuthUser fallback, auth-not-required caseClientPermissionsTests- 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 logicserver/accounts.go- Account struct and managementserver/client.go- CONNECT processing, RegisterUser, RegisterNkeyUser, permission checksserver/nkey.go- Nonce generationserver/opts.go- Auth configuration parsing