From 562f89744da6de972fa738c820a54cba63798605 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Feb 2026 22:24:53 -0500 Subject: [PATCH] feat: add IAuthenticator interface and TokenAuthenticator with constant-time comparison --- src/NATS.Server/Auth/AuthResult.cs | 9 +++ src/NATS.Server/Auth/IAuthenticator.cs | 14 +++++ src/NATS.Server/Auth/TokenAuthenticator.cs | 28 +++++++++ .../TokenAuthenticatorTests.cs | 62 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 src/NATS.Server/Auth/AuthResult.cs create mode 100644 src/NATS.Server/Auth/IAuthenticator.cs create mode 100644 src/NATS.Server/Auth/TokenAuthenticator.cs create mode 100644 tests/NATS.Server.Tests/TokenAuthenticatorTests.cs diff --git a/src/NATS.Server/Auth/AuthResult.cs b/src/NATS.Server/Auth/AuthResult.cs new file mode 100644 index 0000000..9e2d93c --- /dev/null +++ b/src/NATS.Server/Auth/AuthResult.cs @@ -0,0 +1,9 @@ +namespace NATS.Server.Auth; + +public sealed class AuthResult +{ + public required string Identity { get; init; } + public string? AccountName { get; init; } + public Permissions? Permissions { get; init; } + public DateTimeOffset? Expiry { get; init; } +} diff --git a/src/NATS.Server/Auth/IAuthenticator.cs b/src/NATS.Server/Auth/IAuthenticator.cs new file mode 100644 index 0000000..32a5788 --- /dev/null +++ b/src/NATS.Server/Auth/IAuthenticator.cs @@ -0,0 +1,14 @@ +using NATS.Server.Protocol; + +namespace NATS.Server.Auth; + +public interface IAuthenticator +{ + AuthResult? Authenticate(ClientAuthContext context); +} + +public sealed class ClientAuthContext +{ + public required ClientOptions Opts { get; init; } + public required byte[] Nonce { get; init; } +} diff --git a/src/NATS.Server/Auth/TokenAuthenticator.cs b/src/NATS.Server/Auth/TokenAuthenticator.cs new file mode 100644 index 0000000..8500680 --- /dev/null +++ b/src/NATS.Server/Auth/TokenAuthenticator.cs @@ -0,0 +1,28 @@ +using System.Security.Cryptography; +using System.Text; + +namespace NATS.Server.Auth; + +public sealed class TokenAuthenticator : IAuthenticator +{ + private readonly byte[] _expectedToken; + + public TokenAuthenticator(string token) + { + _expectedToken = Encoding.UTF8.GetBytes(token); + } + + public AuthResult? Authenticate(ClientAuthContext context) + { + var clientToken = context.Opts.Token; + if (string.IsNullOrEmpty(clientToken)) + return null; + + var clientBytes = Encoding.UTF8.GetBytes(clientToken); + + if (!CryptographicOperations.FixedTimeEquals(clientBytes, _expectedToken)) + return null; + + return new AuthResult { Identity = "token" }; + } +} diff --git a/tests/NATS.Server.Tests/TokenAuthenticatorTests.cs b/tests/NATS.Server.Tests/TokenAuthenticatorTests.cs new file mode 100644 index 0000000..0c680c6 --- /dev/null +++ b/tests/NATS.Server.Tests/TokenAuthenticatorTests.cs @@ -0,0 +1,62 @@ +using NATS.Server.Auth; +using NATS.Server.Protocol; + +namespace NATS.Server.Tests; + +public class TokenAuthenticatorTests +{ + [Fact] + public void Returns_result_for_correct_token() + { + var auth = new TokenAuthenticator("secret-token"); + var ctx = new ClientAuthContext + { + Opts = new ClientOptions { Token = "secret-token" }, + Nonce = [], + }; + + var result = auth.Authenticate(ctx); + + result.ShouldNotBeNull(); + result.Identity.ShouldBe("token"); + } + + [Fact] + public void Returns_null_for_wrong_token() + { + var auth = new TokenAuthenticator("secret-token"); + var ctx = new ClientAuthContext + { + Opts = new ClientOptions { Token = "wrong-token" }, + Nonce = [], + }; + + auth.Authenticate(ctx).ShouldBeNull(); + } + + [Fact] + public void Returns_null_when_no_token_provided() + { + var auth = new TokenAuthenticator("secret-token"); + var ctx = new ClientAuthContext + { + Opts = new ClientOptions(), + Nonce = [], + }; + + auth.Authenticate(ctx).ShouldBeNull(); + } + + [Fact] + public void Returns_null_for_different_length_token() + { + var auth = new TokenAuthenticator("secret-token"); + var ctx = new ClientAuthContext + { + Opts = new ClientOptions { Token = "short" }, + Nonce = [], + }; + + auth.Authenticate(ctx).ShouldBeNull(); + } +}