feat: session B — auth implementation + signals (26 stubs complete)

Implement ConfigureAuthorization, CheckAuthentication, and full auth
dispatch in NatsServer.Auth.cs; add HandleSignals in NatsServer.Signals.cs;
extend AuthHandler with GetAuthErrClosedState, ValidateProxies,
GetTlsAuthDcs, CheckClientTlsCertSubject, ProcessUserPermissionsTemplate;
add ReadOperatorJwt/ValidateTrustedOperators to JwtProcessor; add
AuthCallout stub; add auth accessor helpers to ClientConnection; add
NATS.NKeys package for NKey signature verification; 12 new tests pass.
This commit is contained in:
Joseph Doherty
2026-02-26 17:38:46 -05:00
parent aa1fb5ac4e
commit 8c380e7ca6
13 changed files with 854 additions and 28 deletions

View File

@@ -0,0 +1,113 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;
using Shouldly;
using Xunit;
public class AuthHandlerExtendedTests
{
[Fact]
public void ValidateProxies_ProxyRequiredWithoutProtocol_ReturnsError()
{
var opts = new ServerOptions { ProxyRequired = true, ProxyProtocol = false };
var err = AuthHandler.ValidateProxies(opts);
err.ShouldNotBeNull();
err!.Message.ShouldContain("proxy_required");
}
[Fact]
public void ValidateProxies_ProxyRequiredWithProtocol_ReturnsNull()
{
var opts = new ServerOptions { ProxyRequired = true, ProxyProtocol = true };
var err = AuthHandler.ValidateProxies(opts);
err.ShouldBeNull();
}
[Fact]
public void GetAuthErrClosedState_ExpiredMessage_ReturnsExpiredState()
{
var err = new InvalidOperationException("token is expired");
AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthenticationExpired);
}
[Fact]
public void GetAuthErrClosedState_NullError_ReturnsTimeout()
{
AuthHandler.GetAuthErrClosedState(null).ShouldBe(ClosedState.AuthenticationTimeout);
}
[Fact]
public void GetAuthErrClosedState_RevokedMessage_ReturnsRevoked()
{
var err = new InvalidOperationException("credential was revoked");
AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthRevoked);
}
[Fact]
public void CheckClientTlsCertSubject_NullCert_ReturnsFalse()
{
AuthHandler.CheckClientTlsCertSubject(null, _ => true).ShouldBeFalse();
}
[Fact]
public void ProcessUserPermissionsTemplate_ExpandsAccountVariable()
{
var lim = new Permissions
{
Publish = new SubjectPermission { Allow = new List<string> { "{{account}}.events" } },
};
var (result, err) = AuthHandler.ProcessUserPermissionsTemplate(lim, "myaccount", null);
err.ShouldBeNull();
result.Publish!.Allow![0].ShouldBe("myaccount.events");
}
[Fact]
public void ProcessUserPermissionsTemplate_ExpandsTagVariable()
{
var lim = new Permissions
{
Subscribe = new SubjectPermission { Allow = new List<string> { "{{tag.region}}.alerts" } },
};
var tags = new Dictionary<string, string> { ["region"] = "us-east" };
var (result, err) = AuthHandler.ProcessUserPermissionsTemplate(lim, "acc", tags);
err.ShouldBeNull();
result.Subscribe!.Allow![0].ShouldBe("us-east.alerts");
}
}
public class JwtProcessorOperatorTests
{
[Fact]
public void ReadOperatorJwtInternal_EmptyString_ReturnsError()
{
var (claims, err) = JwtProcessor.ReadOperatorJwtInternal(string.Empty);
claims.ShouldBeNull();
err.ShouldNotBeNull();
}
[Fact]
public void ReadOperatorJwtInternal_InvalidPrefix_ReturnsFormatError()
{
var (claims, err) = JwtProcessor.ReadOperatorJwtInternal("NOTAJWT.payload.sig");
claims.ShouldBeNull();
err.ShouldBeOfType<FormatException>();
}
[Fact]
public void ReadOperatorJwt_FileNotFound_ReturnsError()
{
var (claims, err) = JwtProcessor.ReadOperatorJwt("/nonexistent/operator.jwt");
claims.ShouldBeNull();
err.ShouldBeOfType<IOException>();
}
[Fact]
public void ValidateTrustedOperators_EmptyList_ReturnsNull()
{
var opts = new ServerOptions();
JwtProcessor.ValidateTrustedOperators(opts).ShouldBeNull();
}
}