Covers auth models, strategy pattern with pluggable authenticators, permission enforcement, core account isolation, and server integration.
287 lines
8.7 KiB
Markdown
287 lines
8.7 KiB
Markdown
# 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
|