diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs
new file mode 100644
index 0000000..2729484
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs
@@ -0,0 +1,273 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/auth.go (standalone functions) in the NATS server Go source.
+
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ZB.MOM.NatsNet.Server.Auth;
+
+///
+/// Authentication helper methods ported from Go auth.go.
+/// Server-dependent methods (configureAuthorization, checkAuthentication, etc.)
+/// will be added in later sessions when the full Server type is available.
+///
+public static partial class AuthHandler
+{
+ ///
+ /// Regex matching valid bcrypt password prefixes ($2a$, $2b$, $2x$, $2y$).
+ /// Mirrors Go validBcryptPrefix.
+ ///
+ private static readonly Regex ValidBcryptPrefix = ValidBcryptPrefixRegex();
+
+ [GeneratedRegex(@"^\$2[abxy]\$\d{2}\$.*")]
+ private static partial Regex ValidBcryptPrefixRegex();
+
+ ///
+ /// Checks if a password string is a bcrypt hash.
+ /// Mirrors Go isBcrypt.
+ ///
+ public static bool IsBcrypt(string password)
+ {
+ if (password.StartsWith('$'))
+ {
+ return ValidBcryptPrefix.IsMatch(password);
+ }
+ return false;
+ }
+
+ ///
+ /// Compares a server password (possibly bcrypt-hashed) against a client-provided password.
+ /// Uses constant-time comparison for plaintext passwords.
+ /// Mirrors Go comparePasswords.
+ ///
+ public static bool ComparePasswords(string serverPassword, string clientPassword)
+ {
+ if (IsBcrypt(serverPassword))
+ {
+ return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword);
+ }
+
+ // Constant-time comparison for plaintext passwords.
+ var spass = Encoding.UTF8.GetBytes(serverPassword);
+ var cpass = Encoding.UTF8.GetBytes(clientPassword);
+ return CryptographicOperations.FixedTimeEquals(spass, cpass);
+ }
+
+ ///
+ /// Validates the ResponsePermission defaults within a Permissions struct.
+ /// If Response is set but MaxMsgs/Expires are zero, applies defaults.
+ /// Also ensures Publish is set with an empty Allow if not already defined.
+ /// Mirrors Go validateResponsePermissions.
+ ///
+ public static void ValidateResponsePermissions(Permissions? p)
+ {
+ if (p?.Response == null)
+ {
+ return;
+ }
+
+ p.Publish ??= new SubjectPermission();
+ p.Publish.Allow ??= [];
+
+ if (p.Response.MaxMsgs == 0)
+ {
+ p.Response.MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs;
+ }
+ if (p.Response.Expires == TimeSpan.Zero)
+ {
+ p.Response.Expires = ServerConstants.DefaultAllowResponseExpiration;
+ }
+ }
+
+ ///
+ /// Known connection type strings (uppercased).
+ /// Mirrors Go jwt.ConnectionType* constants.
+ ///
+ public static class ConnectionTypes
+ {
+ public const string Standard = "STANDARD";
+ public const string Websocket = "WEBSOCKET";
+ public const string Leafnode = "LEAFNODE";
+ public const string LeafnodeWs = "LEAFNODE_WS";
+ public const string Mqtt = "MQTT";
+ public const string MqttWs = "MQTT_WS";
+ public const string InProcess = "IN_PROCESS";
+
+ private static readonly HashSet Known =
+ [
+ Standard,
+ Websocket,
+ Leafnode,
+ LeafnodeWs,
+ Mqtt,
+ MqttWs,
+ InProcess,
+ ];
+
+ public static bool IsKnown(string ct) => Known.Contains(ct);
+ }
+
+ ///
+ /// Validates allowed connection type map entries. Normalises to uppercase
+ /// and rejects unknown types.
+ /// Mirrors Go validateAllowedConnectionTypes.
+ ///
+ public static Exception? ValidateAllowedConnectionTypes(HashSet? m)
+ {
+ if (m == null) return null;
+
+ // We must iterate a copy since we may modify the set.
+ var entries = m.ToList();
+ foreach (var ct in entries)
+ {
+ var ctuc = ct.ToUpperInvariant();
+ if (!ConnectionTypes.IsKnown(ctuc))
+ {
+ return new ArgumentException($"unknown connection type \"{ct}\"");
+ }
+ if (ctuc != ct)
+ {
+ m.Remove(ct);
+ m.Add(ctuc);
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Validates the no_auth_user setting against configured users/nkeys.
+ /// Mirrors Go validateNoAuthUser.
+ ///
+ public static Exception? ValidateNoAuthUser(ServerOptions o, string noAuthUser)
+ {
+ if (string.IsNullOrEmpty(noAuthUser))
+ {
+ return null;
+ }
+ if (o.TrustedOperators.Count > 0)
+ {
+ return new InvalidOperationException("no_auth_user not compatible with Trusted Operator");
+ }
+ if (o.Nkeys == null && o.Users == null)
+ {
+ return new InvalidOperationException(
+ $"no_auth_user: \"{noAuthUser}\" present, but users/nkeys are not defined");
+ }
+ if (o.Users != null)
+ {
+ foreach (var u in o.Users)
+ {
+ if (u.Username == noAuthUser) return null;
+ }
+ }
+ if (o.Nkeys != null)
+ {
+ foreach (var u in o.Nkeys)
+ {
+ if (u.Nkey == noAuthUser) return null;
+ }
+ }
+ return new InvalidOperationException(
+ $"no_auth_user: \"{noAuthUser}\" not present as user or nkey in authorization block or account configuration");
+ }
+
+ ///
+ /// Validates the auth section of options: pinned certs, connection types, and no_auth_user.
+ /// Mirrors Go validateAuth.
+ ///
+ public static Exception? ValidateAuth(ServerOptions o)
+ {
+ // validatePinnedCerts will be added when the full server module is ported.
+ if (o.Users != null)
+ {
+ foreach (var u in o.Users)
+ {
+ var err = ValidateAllowedConnectionTypes(u.AllowedConnectionTypes);
+ if (err != null) return err;
+ }
+ }
+ if (o.Nkeys != null)
+ {
+ foreach (var u in o.Nkeys)
+ {
+ var err = ValidateAllowedConnectionTypes(u.AllowedConnectionTypes);
+ if (err != null) return err;
+ }
+ }
+ return ValidateNoAuthUser(o, o.NoAuthUser);
+ }
+
+ ///
+ /// Splits a DNS alt name into lowercase labels.
+ /// Mirrors Go dnsAltNameLabels.
+ ///
+ public static string[] DnsAltNameLabels(string dnsAltName)
+ {
+ return dnsAltName.ToLowerInvariant().Split('.');
+ }
+
+ ///
+ /// Checks if DNS alt name labels match any of the provided URLs (RFC 6125).
+ /// The wildcard '*' only matches the leftmost label.
+ /// Mirrors Go dnsAltNameMatches.
+ ///
+ public static bool DnsAltNameMatches(string[] dnsAltNameLabels, IReadOnlyList urls)
+ {
+ foreach (var url in urls)
+ {
+ if (url == null)
+ {
+ continue;
+ }
+
+ var hostLabels = url.Host.ToLowerInvariant().Split('.');
+
+ // Following RFC 6125: wildcard never matches multiple labels, only leftmost.
+ if (hostLabels.Length != dnsAltNameLabels.Length)
+ {
+ continue;
+ }
+
+ var i = 0;
+ // Only match wildcard on leftmost label.
+ if (dnsAltNameLabels[0] == "*")
+ {
+ i++;
+ }
+
+ var matched = true;
+ for (; i < dnsAltNameLabels.Length; i++)
+ {
+ if (dnsAltNameLabels[i] != hostLabels[i])
+ {
+ matched = false;
+ break;
+ }
+ }
+ if (matched) return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Wipes a byte slice by filling with 'x'. Used for clearing sensitive data.
+ /// Mirrors Go wipeSlice.
+ ///
+ public static void WipeSlice(Span buf)
+ {
+ buf.Fill((byte)'x');
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs
new file mode 100644
index 0000000..0396adf
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs
@@ -0,0 +1,176 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/auth.go (type definitions) in the NATS server Go source.
+
+namespace ZB.MOM.NatsNet.Server.Auth;
+
+///
+/// Represents a user authenticated via NKey.
+/// Mirrors Go NkeyUser struct in auth.go.
+///
+public class NkeyUser
+{
+ public string Nkey { get; set; } = string.Empty;
+ public long Issued { get; set; }
+ public Permissions? Permissions { get; set; }
+ public Account? Account { get; set; }
+ public string SigningKey { get; set; } = string.Empty;
+ public HashSet? AllowedConnectionTypes { get; set; }
+ public bool ProxyRequired { get; set; }
+
+ ///
+ /// Deep-clones this NkeyUser. Account is shared by reference.
+ /// Mirrors Go NkeyUser.clone().
+ ///
+ public NkeyUser? Clone()
+ {
+ var clone = (NkeyUser)MemberwiseClone();
+ // Account is not cloned because it is always by reference to an existing struct.
+ clone.Permissions = Permissions?.Clone();
+
+ if (AllowedConnectionTypes != null)
+ {
+ clone.AllowedConnectionTypes = new HashSet(AllowedConnectionTypes);
+ }
+
+ return clone;
+ }
+}
+
+///
+/// Represents a user with username/password credentials.
+/// Mirrors Go User struct in auth.go.
+///
+public class User
+{
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public Permissions? Permissions { get; set; }
+ public Account? Account { get; set; }
+ public DateTime ConnectionDeadline { get; set; }
+ public HashSet? AllowedConnectionTypes { get; set; }
+ public bool ProxyRequired { get; set; }
+
+ ///
+ /// Deep-clones this User. Account is shared by reference.
+ /// Mirrors Go User.clone().
+ ///
+ public User? Clone()
+ {
+ var clone = (User)MemberwiseClone();
+ // Account is not cloned because it is always by reference to an existing struct.
+ clone.Permissions = Permissions?.Clone();
+
+ if (AllowedConnectionTypes != null)
+ {
+ clone.AllowedConnectionTypes = new HashSet(AllowedConnectionTypes);
+ }
+
+ return clone;
+ }
+}
+
+///
+/// Subject-level allow/deny permission.
+/// Mirrors Go SubjectPermission in auth.go.
+///
+public class SubjectPermission
+{
+ public List? Allow { get; set; }
+ public List? Deny { get; set; }
+
+ ///
+ /// Deep-clones this SubjectPermission.
+ /// Mirrors Go SubjectPermission.clone().
+ ///
+ public SubjectPermission Clone()
+ {
+ var clone = new SubjectPermission();
+ if (Allow != null)
+ {
+ clone.Allow = new List(Allow);
+ }
+ if (Deny != null)
+ {
+ clone.Deny = new List(Deny);
+ }
+ return clone;
+ }
+}
+
+///
+/// Response permission for request-reply patterns.
+/// Mirrors Go ResponsePermission in auth.go.
+///
+public class ResponsePermission
+{
+ public int MaxMsgs { get; set; }
+ public TimeSpan Expires { get; set; }
+}
+
+///
+/// Publish/subscribe permissions container.
+/// Mirrors Go Permissions in auth.go.
+///
+public class Permissions
+{
+ public SubjectPermission? Publish { get; set; }
+ public SubjectPermission? Subscribe { get; set; }
+ public ResponsePermission? Response { get; set; }
+
+ ///
+ /// Deep-clones this Permissions struct.
+ /// Mirrors Go Permissions.clone().
+ ///
+ public Permissions Clone()
+ {
+ var clone = new Permissions();
+ if (Publish != null)
+ {
+ clone.Publish = Publish.Clone();
+ }
+ if (Subscribe != null)
+ {
+ clone.Subscribe = Subscribe.Clone();
+ }
+ if (Response != null)
+ {
+ clone.Response = new ResponsePermission
+ {
+ MaxMsgs = Response.MaxMsgs,
+ Expires = Response.Expires,
+ };
+ }
+ return clone;
+ }
+}
+
+///
+/// Route-level import/export permissions.
+/// Mirrors Go RoutePermissions in auth.go.
+///
+public class RoutePermissions
+{
+ public SubjectPermission? Import { get; set; }
+ public SubjectPermission? Export { get; set; }
+}
+
+///
+/// Stub for Account type. Full implementation in later sessions.
+/// Mirrors Go Account struct.
+///
+public class Account
+{
+ public string Name { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs
new file mode 100644
index 0000000..1d05c3c
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs
@@ -0,0 +1,110 @@
+// Copyright 2016-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/ciphersuites.go in the NATS server Go source.
+
+using System.Net.Security;
+using System.Security.Authentication;
+
+namespace ZB.MOM.NatsNet.Server.Auth;
+
+///
+/// TLS cipher suite and curve preference definitions.
+/// Mirrors Go ciphersuites.go — cipherMap, defaultCipherSuites, curvePreferenceMap,
+/// defaultCurvePreferences.
+///
+public static class CipherSuites
+{
+ ///
+ /// Map of cipher suite names to their values.
+ /// Populated at static init time — mirrors Go init() + cipherMap.
+ ///
+ public static IReadOnlyDictionary CipherMap { get; }
+
+ ///
+ /// Reverse map of cipher suite ID to name.
+ /// Mirrors Go cipherMapByID.
+ ///
+ public static IReadOnlyDictionary CipherMapById { get; }
+
+ static CipherSuites()
+ {
+ // .NET does not have a direct equivalent of Go's tls.CipherSuites() /
+ // tls.InsecureCipherSuites() enumeration. We enumerate the well-known
+ // TLS 1.2 and 1.3 cipher suites defined in the TlsCipherSuite enum.
+ var byName = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var byId = new Dictionary();
+
+ foreach (TlsCipherSuite cs in Enum.GetValues(typeof(TlsCipherSuite)))
+ {
+ var name = cs.ToString();
+ byName.TryAdd(name, cs);
+ byId.TryAdd(cs, name);
+ }
+
+ CipherMap = byName;
+ CipherMapById = byId;
+ }
+
+ ///
+ /// Returns the default set of TLS 1.2 cipher suites.
+ /// .NET manages cipher suite selection at the OS/SChannel/OpenSSL level;
+ /// this list provides the preferred suites for configuration alignment with Go.
+ /// Mirrors Go defaultCipherSuites.
+ ///
+ public static TlsCipherSuite[] DefaultCipherSuites()
+ {
+ // Return commonly-used TLS 1.2 cipher suites in preference order.
+ // TLS 1.3 suites are always enabled in .NET and cannot be individually toggled.
+ return
+ [
+ TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+ TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
+ ];
+ }
+
+ ///
+ /// Supported named curve / key exchange preferences.
+ /// Mirrors Go curvePreferenceMap.
+ ///
+ public static IReadOnlyDictionary CurvePreferenceMap { get; } =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // .NET does not expose individual curve selection in the same way as Go.
+ // These entries exist for configuration-file compatibility and mapping.
+ // Actual curve negotiation is handled by the OS TLS stack.
+ ["X25519"] = new SslApplicationProtocol("X25519"),
+ ["CurveP256"] = new SslApplicationProtocol("CurveP256"),
+ ["CurveP384"] = new SslApplicationProtocol("CurveP384"),
+ ["CurveP521"] = new SslApplicationProtocol("CurveP521"),
+ };
+
+ ///
+ /// Returns the default curve preferences, ordered highest security first.
+ /// Mirrors Go defaultCurvePreferences.
+ ///
+ public static string[] DefaultCurvePreferences()
+ {
+ return
+ [
+ "X25519",
+ "CurveP256",
+ "CurveP384",
+ "CurveP521",
+ ];
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs
new file mode 100644
index 0000000..f294a6e
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs
@@ -0,0 +1,192 @@
+// Copyright 2018-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Adapted from server/jwt.go in the NATS server Go source.
+
+using System.Net;
+
+namespace ZB.MOM.NatsNet.Server.Auth;
+
+///
+/// JWT processing utilities for NATS operator/account/user JWTs.
+/// Mirrors Go jwt.go functions.
+/// Full JWT parsing will be added when a .NET JWT library equivalent is available.
+///
+public static class JwtProcessor
+{
+ ///
+ /// All JWTs once encoded start with this prefix.
+ /// Mirrors Go jwtPrefix.
+ ///
+ public const string JwtPrefix = "eyJ";
+
+ ///
+ /// Wipes a byte slice by filling with 'x', for clearing nkey seed data.
+ /// Mirrors Go wipeSlice.
+ ///
+ public static void WipeSlice(Span buf)
+ {
+ buf.Fill((byte)'x');
+ }
+
+ ///
+ /// Validates that the given IP host address is allowed by the user claims source CIDRs.
+ /// Returns true if the host is within any of the allowed CIDRs, or if no CIDRs are specified.
+ /// Mirrors Go validateSrc.
+ ///
+ public static bool ValidateSrc(IReadOnlyList? srcCidrs, string host)
+ {
+ if (srcCidrs == null)
+ {
+ return false;
+ }
+ if (srcCidrs.Count == 0)
+ {
+ return true;
+ }
+ if (string.IsNullOrEmpty(host))
+ {
+ return false;
+ }
+ if (!IPAddress.TryParse(host, out var ip))
+ {
+ return false;
+ }
+ foreach (var cidr in srcCidrs)
+ {
+ if (TryParseCidr(cidr, out var network, out var prefixLength))
+ {
+ if (IsInSubnet(ip, network, prefixLength))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ return false; // invalid CIDR means invalid JWT
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Validates that the current time falls within any of the allowed time ranges.
+ /// Returns (allowed, remainingDuration).
+ /// Mirrors Go validateTimes.
+ ///
+ public static (bool Allowed, TimeSpan Remaining) ValidateTimes(
+ IReadOnlyList? timeRanges,
+ string? locale = null)
+ {
+ if (timeRanges == null)
+ {
+ return (false, TimeSpan.Zero);
+ }
+ if (timeRanges.Count == 0)
+ {
+ return (true, TimeSpan.Zero);
+ }
+
+ var now = DateTimeOffset.Now;
+ TimeZoneInfo? tz = null;
+ if (!string.IsNullOrEmpty(locale))
+ {
+ try
+ {
+ tz = TimeZoneInfo.FindSystemTimeZoneById(locale);
+ now = TimeZoneInfo.ConvertTime(now, tz);
+ }
+ catch
+ {
+ return (false, TimeSpan.Zero);
+ }
+ }
+
+ foreach (var timeRange in timeRanges)
+ {
+ if (!TimeSpan.TryParse(timeRange.Start, out var startTime) ||
+ !TimeSpan.TryParse(timeRange.End, out var endTime))
+ {
+ return (false, TimeSpan.Zero);
+ }
+
+ var today = now.Date;
+ var start = today + startTime;
+ var end = today + endTime;
+
+ // If start > end, end is on the next day (overnight range).
+ if (startTime > endTime)
+ {
+ end = end.AddDays(1);
+ }
+
+ if (start <= now && now < end)
+ {
+ return (true, end - now);
+ }
+ }
+ return (false, TimeSpan.Zero);
+ }
+
+ private static bool TryParseCidr(string cidr, out IPAddress network, out int prefixLength)
+ {
+ network = IPAddress.None;
+ prefixLength = 0;
+
+ var slashIndex = cidr.IndexOf('/');
+ if (slashIndex < 0) return false;
+
+ var ipPart = cidr.AsSpan(0, slashIndex);
+ var prefixPart = cidr.AsSpan(slashIndex + 1);
+
+ if (!IPAddress.TryParse(ipPart, out var parsedIp)) return false;
+ if (!int.TryParse(prefixPart, out var prefix)) return false;
+
+ network = parsedIp;
+ prefixLength = prefix;
+ return true;
+ }
+
+ private static bool IsInSubnet(IPAddress address, IPAddress network, int prefixLength)
+ {
+ var addrBytes = address.GetAddressBytes();
+ var netBytes = network.GetAddressBytes();
+ if (addrBytes.Length != netBytes.Length) return false;
+
+ var fullBytes = prefixLength / 8;
+ var remainingBits = prefixLength % 8;
+
+ for (var i = 0; i < fullBytes; i++)
+ {
+ if (addrBytes[i] != netBytes[i]) return false;
+ }
+
+ if (remainingBits > 0 && fullBytes < addrBytes.Length)
+ {
+ var mask = (byte)(0xFF << (8 - remainingBits));
+ if ((addrBytes[fullBytes] & mask) != (netBytes[fullBytes] & mask)) return false;
+ }
+
+ return true;
+ }
+}
+
+///
+/// Represents a time-of-day range for user access control.
+/// Mirrors Go jwt.TimeRange.
+///
+public class TimeRange
+{
+ public string Start { get; set; } = string.Empty;
+ public string End { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
index 22921b8..f26f6c3 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
@@ -16,6 +16,7 @@
using System.Net.Security;
using System.Security.Authentication;
using System.Threading;
+using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server;
@@ -109,6 +110,9 @@ public sealed partial class ServerOptions
public bool NoSystemAccount { get; set; }
public AuthCalloutOpts? AuthCallout { get; set; }
public bool AlwaysEnableNonce { get; set; }
+ public List? Users { get; set; }
+ public List? Nkeys { get; set; }
+ public List