feat: add authenticators, Account, and ClientPermissions (Tasks 3-7, 9)
- Account: per-account SubList and client tracking - IAuthenticator interface, AuthResult, ClientAuthContext - TokenAuthenticator: constant-time token comparison - UserPasswordAuthenticator: multi-user with bcrypt/plain support - SimpleUserPasswordAuthenticator: single user/pass config - NKeyAuthenticator: Ed25519 nonce signature verification - ClientPermissions: SubList-based publish/subscribe authorization
This commit is contained in:
66
src/NATS.Server/Auth/NKeyAuthenticator.cs
Normal file
66
src/NATS.Server/Auth/NKeyAuthenticator.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using NATS.NKeys;
|
||||
|
||||
namespace NATS.Server.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates clients using NKey (Ed25519) public-key signature verification.
|
||||
/// The server sends a random nonce in the INFO message. The client signs the nonce
|
||||
/// with their private key and sends the public key + base64-encoded signature in CONNECT.
|
||||
/// The server verifies the signature against the registered NKey users.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reference: golang/nats-server/server/auth.go — checkNKeyAuth
|
||||
/// </remarks>
|
||||
public sealed class NKeyAuthenticator(IEnumerable<NKeyUser> nkeyUsers) : IAuthenticator
|
||||
{
|
||||
private readonly Dictionary<string, NKeyUser> _nkeys = nkeyUsers.ToDictionary(
|
||||
u => u.Nkey,
|
||||
u => u,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
public AuthResult? Authenticate(ClientAuthContext context)
|
||||
{
|
||||
var clientNkey = context.Opts.Nkey;
|
||||
if (string.IsNullOrEmpty(clientNkey))
|
||||
return null;
|
||||
|
||||
if (!_nkeys.TryGetValue(clientNkey, out var nkeyUser))
|
||||
return null;
|
||||
|
||||
var clientSig = context.Opts.Sig;
|
||||
if (string.IsNullOrEmpty(clientSig))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Decode base64 signature (handle both standard and URL-safe base64)
|
||||
byte[] sigBytes;
|
||||
try
|
||||
{
|
||||
sigBytes = Convert.FromBase64String(clientSig);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Try URL-safe base64 by converting to standard base64
|
||||
var padded = clientSig.Replace('-', '+').Replace('_', '/');
|
||||
padded = padded.PadRight(padded.Length + (4 - padded.Length % 4) % 4, '=');
|
||||
sigBytes = Convert.FromBase64String(padded);
|
||||
}
|
||||
|
||||
var kp = KeyPair.FromPublicKey(clientNkey);
|
||||
if (!kp.Verify(context.Nonce, sigBytes))
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AuthResult
|
||||
{
|
||||
Identity = clientNkey,
|
||||
AccountName = nkeyUser.Account,
|
||||
Permissions = nkeyUser.Permissions,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user