using System.Security.Cryptography; using System.Text; namespace NATS.Server.Auth; /// /// Authenticates clients by looking up username in a dictionary and comparing /// the password using bcrypt (for $2-prefixed hashes) or constant-time comparison /// (for plain text passwords). /// Reference: golang/nats-server/server/auth.go checkClientPassword. /// public sealed class UserPasswordAuthenticator : IAuthenticator { private readonly Dictionary _users; public UserPasswordAuthenticator(IEnumerable users) { _users = new Dictionary(StringComparer.Ordinal); foreach (var user in users) _users[user.Username] = user; } public AuthResult? Authenticate(ClientAuthContext context) { var username = context.Opts.Username; if (string.IsNullOrEmpty(username)) return null; if (!_users.TryGetValue(username, out var user)) return null; var clientPassword = context.Opts.Password ?? string.Empty; if (!ComparePasswords(user.Password, clientPassword)) return null; return new AuthResult { Identity = user.Username, AccountName = user.Account, Permissions = user.Permissions, Expiry = user.ConnectionDeadline, }; } private static bool ComparePasswords(string serverPassword, string clientPassword) { if (IsBcrypt(serverPassword)) { try { return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword); } catch { return false; } } var serverBytes = Encoding.UTF8.GetBytes(serverPassword); var clientBytes = Encoding.UTF8.GetBytes(clientPassword); return CryptographicOperations.FixedTimeEquals(serverBytes, clientBytes); } private static bool IsBcrypt(string password) => password.StartsWith("$2"); }