feat: add AuthService orchestrator with priority-ordered authentication

This commit is contained in:
Joseph Doherty
2026-02-22 22:44:58 -05:00
parent 6ebe791c6d
commit 2a2cc6f0a2
2 changed files with 303 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
using System.Security.Cryptography;
namespace NATS.Server.Auth;
/// <summary>
/// Central authentication orchestrator that builds the appropriate authenticators
/// from NatsOptions and tries them in priority order matching the Go server:
/// NKeys > Users > Token > SimpleUserPassword.
/// Reference: golang/nats-server/server/auth.go — checkClientAuth, configureAuthentication.
/// </summary>
public sealed class AuthService
{
private readonly List<IAuthenticator> _authenticators;
private readonly string? _noAuthUser;
private readonly Dictionary<string, User>? _usersMap;
public bool IsAuthRequired { get; }
public bool NonceRequired { get; }
private AuthService(List<IAuthenticator> authenticators, bool authRequired, bool nonceRequired,
string? noAuthUser, Dictionary<string, User>? usersMap)
{
_authenticators = authenticators;
IsAuthRequired = authRequired;
NonceRequired = nonceRequired;
_noAuthUser = noAuthUser;
_usersMap = usersMap;
}
public static AuthService Build(NatsOptions options)
{
var authenticators = new List<IAuthenticator>();
var authRequired = false;
var nonceRequired = false;
Dictionary<string, User>? usersMap = null;
// Priority order (matching Go): NKeys > Users > Token > SimpleUserPassword
if (options.NKeys is { Count: > 0 })
{
authenticators.Add(new NKeyAuthenticator(options.NKeys));
authRequired = true;
nonceRequired = true;
}
if (options.Users is { Count: > 0 })
{
authenticators.Add(new UserPasswordAuthenticator(options.Users));
authRequired = true;
usersMap = new Dictionary<string, User>(StringComparer.Ordinal);
foreach (var u in options.Users)
usersMap[u.Username] = u;
}
if (!string.IsNullOrEmpty(options.Authorization))
{
authenticators.Add(new TokenAuthenticator(options.Authorization));
authRequired = true;
}
if (!string.IsNullOrEmpty(options.Username) && !string.IsNullOrEmpty(options.Password))
{
authenticators.Add(new SimpleUserPasswordAuthenticator(options.Username, options.Password));
authRequired = true;
}
return new AuthService(authenticators, authRequired, nonceRequired, options.NoAuthUser, usersMap);
}
public AuthResult? Authenticate(ClientAuthContext context)
{
if (!IsAuthRequired)
return new AuthResult { Identity = string.Empty };
foreach (var authenticator in _authenticators)
{
var result = authenticator.Authenticate(context);
if (result != null)
return result;
}
if (_noAuthUser != null && IsNoCredentials(context))
return ResolveNoAuthUser();
return null;
}
private static bool IsNoCredentials(ClientAuthContext context)
{
var opts = context.Opts;
return string.IsNullOrEmpty(opts.Username)
&& string.IsNullOrEmpty(opts.Password)
&& string.IsNullOrEmpty(opts.Token)
&& string.IsNullOrEmpty(opts.Nkey)
&& string.IsNullOrEmpty(opts.Sig);
}
private AuthResult? ResolveNoAuthUser()
{
if (_noAuthUser == null)
return null;
if (_usersMap != null && _usersMap.TryGetValue(_noAuthUser, out var user))
{
return new AuthResult
{
Identity = user.Username,
AccountName = user.Account,
Permissions = user.Permissions,
Expiry = user.ConnectionDeadline,
};
}
return new AuthResult { Identity = _noAuthUser };
}
public byte[] GenerateNonce()
{
Span<byte> raw = stackalloc byte[11];
RandomNumberGenerator.Fill(raw);
return raw.ToArray();
}
public string EncodeNonce(byte[] nonce)
{
return Convert.ToBase64String(nonce)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}