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:
Joseph Doherty
2026-02-26 12:27:33 -05:00
parent ed78a100e2
commit 0a54d342ba
12 changed files with 1698 additions and 8 deletions

View File

@@ -0,0 +1,384 @@
// 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.
using Shouldly;
using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
/// <summary>
/// Tests for AuthHandler standalone functions.
/// Mirrors Go auth_test.go and adds unit tests for validators.
/// </summary>
public class AuthHandlerTests
{
// =========================================================================
// IsBcrypt
// =========================================================================
[Theory]
[InlineData("$2a$10$abc123", true)]
[InlineData("$2b$12$xyz", true)]
[InlineData("$2x$04$foo", true)]
[InlineData("$2y$10$bar", true)]
[InlineData("$3a$10$abc", false)]
[InlineData("plaintext", false)]
[InlineData("", false)]
[InlineData("$2a", false)]
public void IsBcrypt_DetectsCorrectly(string password, bool expected)
{
AuthHandler.IsBcrypt(password).ShouldBe(expected);
}
// =========================================================================
// ComparePasswords
// =========================================================================
[Fact]
public void ComparePasswords_PlaintextMatch()
{
AuthHandler.ComparePasswords("secret", "secret").ShouldBeTrue();
}
[Fact]
public void ComparePasswords_PlaintextMismatch()
{
AuthHandler.ComparePasswords("secret", "wrong").ShouldBeFalse();
}
[Fact]
public void ComparePasswords_BcryptMatch()
{
var hash = BCrypt.Net.BCrypt.HashPassword("mypassword");
AuthHandler.ComparePasswords(hash, "mypassword").ShouldBeTrue();
}
[Fact]
public void ComparePasswords_BcryptMismatch()
{
var hash = BCrypt.Net.BCrypt.HashPassword("mypassword");
AuthHandler.ComparePasswords(hash, "wrongpassword").ShouldBeFalse();
}
[Fact]
public void ComparePasswords_EmptyPasswords_Match()
{
AuthHandler.ComparePasswords("", "").ShouldBeTrue();
}
[Fact]
public void ComparePasswords_DifferentLengths_Mismatch()
{
AuthHandler.ComparePasswords("short", "longpassword").ShouldBeFalse();
}
// =========================================================================
// ValidateResponsePermissions
// =========================================================================
[Fact]
public void ValidateResponsePermissions_NullPermissions_NoOp()
{
AuthHandler.ValidateResponsePermissions(null);
}
[Fact]
public void ValidateResponsePermissions_NullResponse_NoOp()
{
var perms = new Permissions();
AuthHandler.ValidateResponsePermissions(perms);
perms.Publish.ShouldBeNull();
}
[Fact]
public void ValidateResponsePermissions_SetsDefaults()
{
var perms = new Permissions
{
Response = new ResponsePermission(),
};
AuthHandler.ValidateResponsePermissions(perms);
perms.Response.MaxMsgs.ShouldBe(ServerConstants.DefaultAllowResponseMaxMsgs);
perms.Response.Expires.ShouldBe(ServerConstants.DefaultAllowResponseExpiration);
perms.Publish.ShouldNotBeNull();
perms.Publish!.Allow.ShouldNotBeNull();
perms.Publish.Allow!.Count.ShouldBe(0);
}
[Fact]
public void ValidateResponsePermissions_PreservesExistingValues()
{
var perms = new Permissions
{
Publish = new SubjectPermission { Allow = ["foo.>"] },
Response = new ResponsePermission
{
MaxMsgs = 10,
Expires = TimeSpan.FromMinutes(5),
},
};
AuthHandler.ValidateResponsePermissions(perms);
perms.Response.MaxMsgs.ShouldBe(10);
perms.Response.Expires.ShouldBe(TimeSpan.FromMinutes(5));
perms.Publish.Allow.ShouldBe(["foo.>"]);
}
// =========================================================================
// ValidateAllowedConnectionTypes
// =========================================================================
[Fact]
public void ValidateAllowedConnectionTypes_NullMap_ReturnsNull()
{
AuthHandler.ValidateAllowedConnectionTypes(null).ShouldBeNull();
}
[Fact]
public void ValidateAllowedConnectionTypes_ValidTypes_NoError()
{
var m = new HashSet<string> { "STANDARD", "WEBSOCKET" };
AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
}
[Fact]
public void ValidateAllowedConnectionTypes_UnknownType_ReturnsError()
{
var m = new HashSet<string> { "STANDARD", "someNewType" };
var err = AuthHandler.ValidateAllowedConnectionTypes(m);
err.ShouldNotBeNull();
err!.Message.ShouldContain("connection type");
}
[Fact]
public void ValidateAllowedConnectionTypes_NormalizesToUppercase()
{
var m = new HashSet<string> { "websocket", "mqtt" };
AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
m.ShouldContain("WEBSOCKET");
m.ShouldContain("MQTT");
m.ShouldNotContain("websocket");
m.ShouldNotContain("mqtt");
}
[Fact]
public void ValidateAllowedConnectionTypes_AllKnownTypes_NoError()
{
var m = new HashSet<string>
{
"STANDARD", "WEBSOCKET", "LEAFNODE",
"LEAFNODE_WS", "MQTT", "MQTT_WS", "IN_PROCESS",
};
AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
}
// =========================================================================
// ValidateNoAuthUser
// =========================================================================
[Fact]
public void ValidateNoAuthUser_EmptyNoAuthUser_NoError()
{
var opts = new ServerOptions();
AuthHandler.ValidateNoAuthUser(opts, "").ShouldBeNull();
}
[Fact]
public void ValidateNoAuthUser_WithTrustedOperators_ReturnsError()
{
var opts = new ServerOptions
{
TrustedOperators = [new object()],
};
var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
err.ShouldNotBeNull();
err!.Message.ShouldContain("Trusted Operator");
}
[Fact]
public void ValidateNoAuthUser_NoUsersOrNkeys_ReturnsError()
{
var opts = new ServerOptions();
var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
err.ShouldNotBeNull();
err!.Message.ShouldContain("users/nkeys are not defined");
}
[Fact]
public void ValidateNoAuthUser_UserFound_NoError()
{
var opts = new ServerOptions
{
Users = [new User { Username = "foo" }],
};
AuthHandler.ValidateNoAuthUser(opts, "foo").ShouldBeNull();
}
[Fact]
public void ValidateNoAuthUser_NkeyFound_NoError()
{
var opts = new ServerOptions
{
Nkeys = [new NkeyUser { Nkey = "NKEY1" }],
};
AuthHandler.ValidateNoAuthUser(opts, "NKEY1").ShouldBeNull();
}
[Fact]
public void ValidateNoAuthUser_UserNotFound_ReturnsError()
{
var opts = new ServerOptions
{
Users = [new User { Username = "bar" }],
};
var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
err.ShouldNotBeNull();
err!.Message.ShouldContain("not present as user or nkey");
}
// =========================================================================
// ValidateAuth
// =========================================================================
[Fact]
public void ValidateAuth_ValidConfig_NoError()
{
var opts = new ServerOptions
{
Users =
[
new User
{
Username = "u1",
AllowedConnectionTypes = new HashSet<string> { "STANDARD" },
},
],
NoAuthUser = "u1",
};
AuthHandler.ValidateAuth(opts).ShouldBeNull();
}
[Fact]
public void ValidateAuth_InvalidConnectionType_ReturnsError()
{
var opts = new ServerOptions
{
Users =
[
new User
{
Username = "u1",
AllowedConnectionTypes = new HashSet<string> { "STANDARD", "BAD_TYPE" },
},
],
};
var err = AuthHandler.ValidateAuth(opts);
err.ShouldNotBeNull();
err!.Message.ShouldContain("connection type");
}
// =========================================================================
// DnsAltNameLabels + DnsAltNameMatches — Go test ID 148
// =========================================================================
[Theory]
[InlineData("foo", new[] { "nats://FOO" }, true)]
[InlineData("foo", new[] { "nats://.." }, false)]
[InlineData("foo", new[] { "nats://." }, false)]
[InlineData("Foo", new[] { "nats://foO" }, true)]
[InlineData("FOO", new[] { "nats://foo" }, true)]
[InlineData("foo1", new[] { "nats://bar" }, false)]
[InlineData("multi", new[] { "nats://m", "nats://mu", "nats://mul", "nats://multi" }, true)]
[InlineData("multi", new[] { "nats://multi", "nats://m", "nats://mu", "nats://mul" }, true)]
[InlineData("foo.bar", new[] { "nats://foo", "nats://foo.bar.bar", "nats://foo.baz" }, false)]
[InlineData("foo.Bar", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, true)]
[InlineData("foo.*", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, false)]
[InlineData("f*.bar", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, false)]
[InlineData("*.bar", new[] { "nats://foo.bar" }, true)]
[InlineData("*", new[] { "nats://baz.bar", "nats://bar", "nats://z.y" }, true)]
[InlineData("*", new[] { "nats://bar" }, true)]
[InlineData("*", new[] { "nats://." }, false)]
[InlineData("*", new[] { "nats://" }, true)]
// NOTE: Go test cases {"*", ["*"], true} and {"bar.*", ["bar.*"], true} are omitted
// because .NET's Uri class does not preserve '*' in hostnames the same way Go's url.Parse does.
// Similarly, cases with leading dots like ".Y.local" and "..local" are omitted
// because .NET's Uri normalizes those hostnames differently.
[InlineData("*.Y-X-red-mgmt.default.svc", new[] { "nats://A.Y-X-red-mgmt.default.svc" }, true)]
[InlineData("*.Y-X-green-mgmt.default.svc", new[] { "nats://A.Y-X-green-mgmt.default.svc" }, true)]
[InlineData("*.Y-X-blue-mgmt.default.svc", new[] { "nats://A.Y-X-blue-mgmt.default.svc" }, true)]
[InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-red-mgmt" }, true)]
[InlineData("Y-X-red-mgmt", new[] { "nats://X-X-red-mgmt" }, false)]
[InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-green-mgmt" }, false)]
[InlineData("Y-X-red-mgmt", new[] { "nats://Y" }, false)]
[InlineData("Y-X-red-mgmt", new[] { "nats://Y-X" }, false)]
[InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-red" }, false)]
[InlineData("Y-X-red-mgmt", new[] { "nats://X-red-mgmt" }, false)]
[InlineData("Y-X-green-mgmt", new[] { "nats://Y-X-green-mgmt" }, true)]
[InlineData("Y-X-blue-mgmt", new[] { "nats://Y-X-blue-mgmt" }, true)]
[InlineData("connect.Y.local", new[] { "nats://connect.Y.local" }, true)]
[InlineData("gcp.Y.local", new[] { "nats://gcp.Y.local" }, true)]
[InlineData("uswest1.gcp.Y.local", new[] { "nats://uswest1.gcp.Y.local" }, true)]
public void DnsAltNameMatches_MatchesCorrectly(string altName, string[] urlStrings, bool expected)
{
var urls = urlStrings.Select(s => new Uri(s)).ToArray();
var labels = AuthHandler.DnsAltNameLabels(altName);
AuthHandler.DnsAltNameMatches(labels, urls).ShouldBe(expected);
}
[Fact]
public void DnsAltNameMatches_NullUrl_Skipped()
{
var labels = AuthHandler.DnsAltNameLabels("foo");
var urls = new Uri?[] { null, new Uri("nats://foo") };
AuthHandler.DnsAltNameMatches(labels, urls!).ShouldBeTrue();
}
// =========================================================================
// WipeSlice
// =========================================================================
[Fact]
public void WipeSlice_FillsWithX()
{
var buf = new byte[] { 1, 2, 3, 4, 5 };
AuthHandler.WipeSlice(buf);
buf.ShouldAllBe(b => b == (byte)'x');
}
[Fact]
public void WipeSlice_EmptyBuffer_NoOp()
{
var buf = Array.Empty<byte>();
AuthHandler.WipeSlice(buf); // should not throw
}
// =========================================================================
// ConnectionTypes known check
// =========================================================================
[Theory]
[InlineData("STANDARD", true)]
[InlineData("WEBSOCKET", true)]
[InlineData("LEAFNODE", true)]
[InlineData("LEAFNODE_WS", true)]
[InlineData("MQTT", true)]
[InlineData("MQTT_WS", true)]
[InlineData("IN_PROCESS", true)]
[InlineData("UNKNOWN", false)]
[InlineData("", false)]
public void ConnectionTypes_IsKnown_DetectsCorrectly(string ct, bool expected)
{
AuthHandler.ConnectionTypes.IsKnown(ct).ShouldBe(expected);
}
}

View File

@@ -0,0 +1,246 @@
// 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.
using Shouldly;
using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
/// <summary>
/// Tests for auth type cloning and validation.
/// Mirrors Go auth_test.go: TestUserClone*, TestDNSAltNameMatching, and additional unit tests.
/// </summary>
public class AuthTypesTests
{
// -------------------------------------------------------------------------
// TestUserCloneNilPermissions — Go test ID 142
// -------------------------------------------------------------------------
[Fact]
public void UserClone_NilPermissions_ProducesDeepCopy()
{
var user = new User { Username = "foo", Password = "bar" };
var clone = user.Clone()!;
clone.Username.ShouldBe(user.Username);
clone.Password.ShouldBe(user.Password);
clone.Permissions.ShouldBeNull();
// Mutation should not affect original.
clone.Password = "baz";
user.Password.ShouldBe("bar");
}
// -------------------------------------------------------------------------
// TestUserClone — Go test ID 143
// -------------------------------------------------------------------------
[Fact]
public void UserClone_WithPermissions_ProducesDeepCopy()
{
var user = new User
{
Username = "foo",
Password = "bar",
Permissions = new Permissions
{
Publish = new SubjectPermission { Allow = ["foo"] },
Subscribe = new SubjectPermission { Allow = ["bar"] },
},
};
var clone = user.Clone()!;
clone.Username.ShouldBe("foo");
clone.Permissions.ShouldNotBeNull();
clone.Permissions!.Publish!.Allow.ShouldBe(["foo"]);
clone.Permissions.Subscribe!.Allow.ShouldBe(["bar"]);
// Mutating clone should not affect original.
clone.Permissions.Subscribe.Allow = ["baz"];
user.Permissions!.Subscribe!.Allow.ShouldBe(["bar"]);
}
// -------------------------------------------------------------------------
// TestUserClonePermissionsNoLists — Go test ID 144
// -------------------------------------------------------------------------
[Fact]
public void UserClone_EmptyPermissions_PublishSubscribeAreNull()
{
var user = new User
{
Username = "foo",
Password = "bar",
Permissions = new Permissions(),
};
var clone = user.Clone()!;
clone.Permissions.ShouldNotBeNull();
clone.Permissions!.Publish.ShouldBeNull();
clone.Permissions.Subscribe.ShouldBeNull();
}
// -------------------------------------------------------------------------
// TestUserCloneNoPermissions — Go test ID 145
// -------------------------------------------------------------------------
[Fact]
public void UserClone_NoPermissions_PermissionsIsNull()
{
var user = new User { Username = "foo", Password = "bar" };
var clone = user.Clone()!;
clone.Permissions.ShouldBeNull();
}
// -------------------------------------------------------------------------
// TestUserCloneNil — Go test ID 146
// -------------------------------------------------------------------------
[Fact]
public void UserClone_NilUser_ReturnsNull()
{
User? user = null;
var clone = user?.Clone();
clone.ShouldBeNull();
}
// -------------------------------------------------------------------------
// NkeyUser clone tests (additional coverage for NkeyUser.clone)
// -------------------------------------------------------------------------
[Fact]
public void NkeyUserClone_WithAllowedConnectionTypes_ProducesDeepCopy()
{
var nkey = new NkeyUser
{
Nkey = "NKEY123",
SigningKey = "SK1",
AllowedConnectionTypes = new HashSet<string> { "STANDARD", "WEBSOCKET" },
Permissions = new Permissions
{
Publish = new SubjectPermission { Allow = ["pub.>"] },
},
};
var clone = nkey.Clone()!;
clone.Nkey.ShouldBe("NKEY123");
clone.AllowedConnectionTypes.ShouldNotBeNull();
clone.AllowedConnectionTypes!.Count.ShouldBe(2);
clone.Permissions!.Publish!.Allow.ShouldBe(["pub.>"]);
// Mutating clone should not affect original.
clone.AllowedConnectionTypes.Add("MQTT");
nkey.AllowedConnectionTypes.Count.ShouldBe(2);
clone.Permissions.Publish.Allow = ["other.>"];
nkey.Permissions!.Publish!.Allow.ShouldBe(["pub.>"]);
}
[Fact]
public void NkeyUserClone_NilNkeyUser_ReturnsNull()
{
NkeyUser? nkey = null;
var clone = nkey?.Clone();
clone.ShouldBeNull();
}
// -------------------------------------------------------------------------
// SubjectPermission clone tests
// -------------------------------------------------------------------------
[Fact]
public void SubjectPermissionClone_WithAllowAndDeny_ProducesDeepCopy()
{
var perm = new SubjectPermission
{
Allow = ["foo.>", "bar.>"],
Deny = ["baz.>"],
};
var clone = perm.Clone();
clone.Allow.ShouldBe(["foo.>", "bar.>"]);
clone.Deny.ShouldBe(["baz.>"]);
clone.Allow.Add("extra");
perm.Allow.Count.ShouldBe(2);
}
[Fact]
public void SubjectPermissionClone_NullLists_StaysNull()
{
var perm = new SubjectPermission();
var clone = perm.Clone();
clone.Allow.ShouldBeNull();
clone.Deny.ShouldBeNull();
}
// -------------------------------------------------------------------------
// Permissions clone with Response
// -------------------------------------------------------------------------
[Fact]
public void PermissionsClone_WithResponse_ProducesDeepCopy()
{
var perms = new Permissions
{
Publish = new SubjectPermission { Allow = ["pub"] },
Subscribe = new SubjectPermission { Deny = ["deny"] },
Response = new ResponsePermission { MaxMsgs = 5, Expires = TimeSpan.FromMinutes(1) },
};
var clone = perms.Clone();
clone.Response.ShouldNotBeNull();
clone.Response!.MaxMsgs.ShouldBe(5);
clone.Response.Expires.ShouldBe(TimeSpan.FromMinutes(1));
// Mutating clone should not affect original.
clone.Response.MaxMsgs = 10;
perms.Response!.MaxMsgs.ShouldBe(5);
}
// -------------------------------------------------------------------------
// User clone with AllowedConnectionTypes
// -------------------------------------------------------------------------
[Fact]
public void UserClone_WithAllowedConnectionTypes_ProducesDeepCopy()
{
var user = new User
{
Username = "u1",
AllowedConnectionTypes = new HashSet<string> { "STANDARD" },
};
var clone = user.Clone()!;
clone.AllowedConnectionTypes.ShouldNotBeNull();
clone.AllowedConnectionTypes!.ShouldContain("STANDARD");
clone.AllowedConnectionTypes.Add("MQTT");
user.AllowedConnectionTypes.Count.ShouldBe(1);
}
// -------------------------------------------------------------------------
// User clone with Account (shared by reference)
// -------------------------------------------------------------------------
[Fact]
public void UserClone_AccountSharedByReference()
{
var acct = new Account { Name = "TestAccount" };
var user = new User { Username = "u1", Account = acct };
var clone = user.Clone()!;
// Account should be same reference.
clone.Account.ShouldBeSameAs(acct);
}
}

View File

@@ -0,0 +1,82 @@
// 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.
using System.Security.Authentication;
using System.Net.Security;
using Shouldly;
using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
/// <summary>
/// Tests for CipherSuites definitions.
/// Mirrors Go ciphersuites.go functionality.
/// </summary>
public class CipherSuitesTests
{
[Fact]
public void CipherMap_ContainsTls13Suites()
{
CipherSuites.CipherMap.ShouldNotBeEmpty();
// At minimum, TLS 1.3 suites should be present.
CipherSuites.CipherMap.ShouldContainKey("TLS_AES_256_GCM_SHA384");
CipherSuites.CipherMap.ShouldContainKey("TLS_AES_128_GCM_SHA256");
}
[Fact]
public void CipherMapById_ContainsTls13Suites()
{
CipherSuites.CipherMapById.ShouldNotBeEmpty();
CipherSuites.CipherMapById.ShouldContainKey(TlsCipherSuite.TLS_AES_256_GCM_SHA384);
}
[Fact]
public void CipherMap_CaseInsensitiveLookup()
{
// The map uses OrdinalIgnoreCase comparer.
CipherSuites.CipherMap.ShouldContainKey("tls_aes_256_gcm_sha384");
}
[Fact]
public void DefaultCipherSuites_ReturnsNonEmptyList()
{
var defaults = CipherSuites.DefaultCipherSuites();
defaults.ShouldNotBeEmpty();
defaults.Length.ShouldBeGreaterThan(0);
}
[Fact]
public void DefaultCipherSuites_ContainsSecureSuites()
{
var defaults = CipherSuites.DefaultCipherSuites();
defaults.ShouldContain(TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384);
defaults.ShouldContain(TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384);
}
[Fact]
public void CurvePreferenceMap_ContainsExpectedCurves()
{
CipherSuites.CurvePreferenceMap.ShouldContainKey("X25519");
CipherSuites.CurvePreferenceMap.ShouldContainKey("CurveP256");
CipherSuites.CurvePreferenceMap.ShouldContainKey("CurveP384");
CipherSuites.CurvePreferenceMap.ShouldContainKey("CurveP521");
}
[Fact]
public void DefaultCurvePreferences_ReturnsExpectedOrder()
{
var prefs = CipherSuites.DefaultCurvePreferences();
prefs.Length.ShouldBeGreaterThanOrEqualTo(4);
prefs[0].ShouldBe("X25519");
}
}

View File

@@ -0,0 +1,184 @@
// 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.
using Shouldly;
using ZB.MOM.NatsNet.Server.Auth;
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
/// <summary>
/// Tests for JwtProcessor functions.
/// Mirrors Go jwt.go functionality for standalone testable functions.
/// </summary>
public class JwtProcessorTests
{
// =========================================================================
// JwtPrefix constant
// =========================================================================
[Fact]
public void JwtPrefix_IsCorrect()
{
JwtProcessor.JwtPrefix.ShouldBe("eyJ");
}
// =========================================================================
// WipeSlice
// =========================================================================
[Fact]
public void WipeSlice_FillsWithX()
{
var buf = new byte[] { 0x01, 0x02, 0x03 };
JwtProcessor.WipeSlice(buf);
buf.ShouldAllBe(b => b == (byte)'x');
}
[Fact]
public void WipeSlice_EmptyBuffer_NoOp()
{
var buf = Array.Empty<byte>();
JwtProcessor.WipeSlice(buf);
}
// =========================================================================
// ValidateSrc
// =========================================================================
[Fact]
public void ValidateSrc_NullCidrs_ReturnsFalse()
{
JwtProcessor.ValidateSrc(null, "192.168.1.1").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_EmptyCidrs_ReturnsTrue()
{
JwtProcessor.ValidateSrc([], "192.168.1.1").ShouldBeTrue();
}
[Fact]
public void ValidateSrc_EmptyHost_ReturnsFalse()
{
JwtProcessor.ValidateSrc(["192.168.0.0/16"], "").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_InvalidHost_ReturnsFalse()
{
JwtProcessor.ValidateSrc(["192.168.0.0/16"], "not-an-ip").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_MatchingCidr_ReturnsTrue()
{
JwtProcessor.ValidateSrc(["192.168.0.0/16"], "192.168.1.100").ShouldBeTrue();
}
[Fact]
public void ValidateSrc_NonMatchingCidr_ReturnsFalse()
{
JwtProcessor.ValidateSrc(["10.0.0.0/8"], "192.168.1.100").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_MultipleCidrs_MatchesAny()
{
var cidrs = new[] { "10.0.0.0/8", "192.168.0.0/16" };
JwtProcessor.ValidateSrc(cidrs, "192.168.1.100").ShouldBeTrue();
JwtProcessor.ValidateSrc(cidrs, "10.1.2.3").ShouldBeTrue();
JwtProcessor.ValidateSrc(cidrs, "172.16.0.1").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_ExactMatch_SingleHost()
{
JwtProcessor.ValidateSrc(["192.168.1.100/32"], "192.168.1.100").ShouldBeTrue();
JwtProcessor.ValidateSrc(["192.168.1.100/32"], "192.168.1.101").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_InvalidCidr_ReturnsFalse()
{
JwtProcessor.ValidateSrc(["not-a-cidr"], "192.168.1.1").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_Ipv6_MatchingCidr()
{
JwtProcessor.ValidateSrc(["::1/128"], "::1").ShouldBeTrue();
JwtProcessor.ValidateSrc(["::1/128"], "::2").ShouldBeFalse();
}
[Fact]
public void ValidateSrc_MismatchedIpFamilies_ReturnsFalse()
{
// IPv6 CIDR with IPv4 address should not match.
JwtProcessor.ValidateSrc(["::1/128"], "127.0.0.1").ShouldBeFalse();
}
// =========================================================================
// ValidateTimes
// =========================================================================
[Fact]
public void ValidateTimes_NullRanges_ReturnsFalse()
{
var (allowed, remaining) = JwtProcessor.ValidateTimes(null);
allowed.ShouldBeFalse();
remaining.ShouldBe(TimeSpan.Zero);
}
[Fact]
public void ValidateTimes_EmptyRanges_ReturnsTrue()
{
var (allowed, remaining) = JwtProcessor.ValidateTimes([]);
allowed.ShouldBeTrue();
remaining.ShouldBe(TimeSpan.Zero);
}
[Fact]
public void ValidateTimes_CurrentTimeInRange_ReturnsTrue()
{
var now = DateTimeOffset.Now;
var start = now.AddMinutes(-30).ToString("HH:mm:ss");
var end = now.AddMinutes(30).ToString("HH:mm:ss");
var ranges = new[] { new TimeRange { Start = start, End = end } };
var (allowed, remaining) = JwtProcessor.ValidateTimes(ranges);
allowed.ShouldBeTrue();
remaining.TotalMinutes.ShouldBeGreaterThan(0);
remaining.TotalMinutes.ShouldBeLessThanOrEqualTo(30);
}
[Fact]
public void ValidateTimes_CurrentTimeOutOfRange_ReturnsFalse()
{
var now = DateTimeOffset.Now;
// Set a range entirely in the past today.
var start = now.AddHours(-3).ToString("HH:mm:ss");
var end = now.AddHours(-2).ToString("HH:mm:ss");
var ranges = new[] { new TimeRange { Start = start, End = end } };
var (allowed, _) = JwtProcessor.ValidateTimes(ranges);
allowed.ShouldBeFalse();
}
[Fact]
public void ValidateTimes_InvalidFormat_ReturnsFalse()
{
var ranges = new[] { new TimeRange { Start = "bad", End = "data" } };
var (allowed, _) = JwtProcessor.ValidateTimes(ranges);
allowed.ShouldBeFalse();
}
}