# 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<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:

```csharp
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

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

```csharp
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

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

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