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
This commit is contained in:
192
dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs
Normal file
192
dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs
Normal file
@@ -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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
Reference in New Issue
Block a user