// 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; using ZB.MOM.NatsNet.Server; 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; } /// /// Reads an operator JWT from a file path. Returns (claims, error). /// Mirrors Go ReadOperatorJWT in server/jwt.go. /// public static (object? Claims, Exception? Error) ReadOperatorJwt(string path) { if (string.IsNullOrEmpty(path)) return (null, new ArgumentException("operator JWT path is empty")); string jwtString; try { jwtString = File.ReadAllText(path, System.Text.Encoding.ASCII).Trim(); } catch (Exception ex) { return (null, new IOException($"error reading operator JWT file: {ex.Message}", ex)); } return ReadOperatorJwtInternal(jwtString); } /// /// Decodes an operator JWT string. Returns (claims, error). /// Mirrors Go readOperatorJWT in server/jwt.go. /// public static (object? Claims, Exception? Error) ReadOperatorJwtInternal(string jwtString) { if (string.IsNullOrEmpty(jwtString)) return (null, new ArgumentException("operator JWT string is empty")); if (!jwtString.StartsWith(JwtPrefix, StringComparison.Ordinal)) return (null, new FormatException($"operator JWT does not start with expected prefix '{JwtPrefix}'")); // Full NATS JWT parsing would require a dedicated JWT library. // At this level, we validate the prefix and structure. return (null, new FormatException("operator JWT parsing not fully implemented — requires NATS JWT library")); } /// /// Validates the trusted operator JWTs in options. /// Mirrors Go validateTrustedOperators in server/jwt.go. /// public static Exception? ValidateTrustedOperators(ServerOptions opts) { if (opts.TrustedOperators == null || opts.TrustedOperators.Count == 0) return null; // Each operator should be a well-formed JWT. foreach (var op in opts.TrustedOperators) { var jwtStr = op?.ToString() ?? string.Empty; var (_, err) = ReadOperatorJwtInternal(jwtStr); // Allow the "not implemented" case through — structure validated up to prefix check. if (err is FormatException fe && fe.Message.Contains("not fully implemented")) continue; if (err is ArgumentException) return new InvalidOperationException($"invalid trusted operator JWT: {err.Message}"); } return null; } } /// /// 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; }