Files
natsdotnet/docs/plans/2026-02-22-authentication-design.md
Joseph Doherty bca703b310 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.
2026-02-22 22:07:16 -05:00

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/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

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)

  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 "<subject>"' on deny.
  • ProcessSub() - check IsSubscribeAllowed(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 AuthService during startup based on options
  • Maintain ConcurrentDictionary<string, Account> 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