using System.Security.Cryptography; namespace NATS.Server.Auth; /// /// 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. /// public sealed class AuthService { private readonly List _authenticators; private readonly string? _noAuthUser; private readonly Dictionary? _usersMap; public bool IsAuthRequired { get; } public bool NonceRequired { get; } private AuthService(List authenticators, bool authRequired, bool nonceRequired, string? noAuthUser, Dictionary? usersMap) { _authenticators = authenticators; IsAuthRequired = authRequired; NonceRequired = nonceRequired; _noAuthUser = noAuthUser; _usersMap = usersMap; } public static AuthService Build(NatsOptions options) { var authenticators = new List(); var authRequired = false; var nonceRequired = false; Dictionary? usersMap = null; // TLS certificate mapping (highest priority when enabled) if (options.TlsMap && options.TlsVerify && options.Users is { Count: > 0 }) { authenticators.Add(new TlsMapAuthenticator(options.Users)); authRequired = true; } // JWT / Operator mode (highest priority after TLS) if (options.TrustedKeys is { Length: > 0 } && options.AccountResolver is not null) { authenticators.Add(new JwtAuthenticator(options.TrustedKeys, options.AccountResolver)); authRequired = true; nonceRequired = true; } // 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(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) && string.IsNullOrEmpty(opts.JWT); } 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 raw = stackalloc byte[11]; RandomNumberGenerator.Fill(raw); return raw.ToArray(); } public string EncodeNonce(byte[] nonce) { return Convert.ToBase64String(nonce) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } }