Files
natsnet/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs
Joseph Doherty 0a54d342ba feat: port session 06 — Authentication & JWT types, validators, cipher suites
Port independently-testable auth functions from auth.go, ciphersuites.go,
and jwt.go. Server-dependent methods (configureAuthorization, checkAuthentication,
auth callout, etc.) are stubbed for later sessions.

- AuthTypes: User, NkeyUser, SubjectPermission, ResponsePermission, Permissions,
  RoutePermissions, Account — all with deep Clone() methods
- AuthHandler: IsBcrypt, ComparePasswords, ValidateResponsePermissions,
  ValidateAllowedConnectionTypes, ValidateNoAuthUser, ValidateAuth,
  DnsAltNameLabels, DnsAltNameMatches, WipeSlice, ConnectionTypes constants
- CipherSuites: CipherMap, CipherMapById, DefaultCipherSuites,
  CurvePreferenceMap, DefaultCurvePreferences
- JwtProcessor: JwtPrefix, WipeSlice, ValidateSrc (CIDR matching),
  ValidateTimes (time-of-day ranges), TimeRange type
- ServerOptions: added Users, Nkeys, TrustedOperators properties
- 67 new unit tests (all 328 tests pass)
- DB: 18 features complete, 25 stubbed; 6 Go tests complete, 125 stubbed
2026-02-26 12:27:33 -05:00

193 lines
5.6 KiB
C#

// 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;
/// <summary>
/// JWT processing utilities for NATS operator/account/user JWTs.
/// Mirrors Go <c>jwt.go</c> functions.
/// Full JWT parsing will be added when a .NET JWT library equivalent is available.
/// </summary>
public static class JwtProcessor
{
/// <summary>
/// All JWTs once encoded start with this prefix.
/// Mirrors Go <c>jwtPrefix</c>.
/// </summary>
public const string JwtPrefix = "eyJ";
/// <summary>
/// Wipes a byte slice by filling with 'x', for clearing nkey seed data.
/// Mirrors Go <c>wipeSlice</c>.
/// </summary>
public static void WipeSlice(Span<byte> buf)
{
buf.Fill((byte)'x');
}
/// <summary>
/// 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 <c>validateSrc</c>.
/// </summary>
public static bool ValidateSrc(IReadOnlyList<string>? 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;
}
/// <summary>
/// Validates that the current time falls within any of the allowed time ranges.
/// Returns (allowed, remainingDuration).
/// Mirrors Go <c>validateTimes</c>.
/// </summary>
public static (bool Allowed, TimeSpan Remaining) ValidateTimes(
IReadOnlyList<TimeRange>? 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;
}
}
/// <summary>
/// Represents a time-of-day range for user access control.
/// Mirrors Go <c>jwt.TimeRange</c>.
/// </summary>
public class TimeRange
{
public string Start { get; set; } = string.Empty;
public string End { get; set; } = string.Empty;
}