refactor: extract NATS.Server.Auth.Tests project
Move 50 auth/accounts/permissions/JWT/NKey test files from NATS.Server.Tests into a dedicated NATS.Server.Auth.Tests project. Update namespaces, replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls, replace Task.Delay with TaskCompletionSource in test doubles, and add InternalsVisibleTo. 690 tests pass.
This commit is contained in:
866
tests/NATS.Server.Auth.Tests/JwtAuthenticatorTests.cs
Normal file
866
tests/NATS.Server.Auth.Tests/JwtAuthenticatorTests.cs
Normal file
@@ -0,0 +1,866 @@
|
||||
using System.Text;
|
||||
using NATS.NKeys;
|
||||
using NATS.Server.Auth;
|
||||
using NATS.Server.Auth.Jwt;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Auth.Tests;
|
||||
|
||||
public class JwtAuthenticatorTests
|
||||
{
|
||||
private static string Base64UrlEncode(string input) =>
|
||||
Base64UrlEncode(Encoding.UTF8.GetBytes(input));
|
||||
|
||||
private static string Base64UrlEncode(byte[] input) =>
|
||||
Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
|
||||
private static string BuildSignedToken(string payloadJson, KeyPair signingKey)
|
||||
{
|
||||
var header = Base64UrlEncode("""{"typ":"JWT","alg":"ed25519-nkey"}""");
|
||||
var payload = Base64UrlEncode(payloadJson);
|
||||
var signingInput = Encoding.UTF8.GetBytes($"{header}.{payload}");
|
||||
var sig = new byte[64];
|
||||
signingKey.Sign(signingInput, sig);
|
||||
return $"{header}.{payload}.{Base64UrlEncode(sig)}";
|
||||
}
|
||||
|
||||
private static string SignNonce(KeyPair kp, byte[] nonce)
|
||||
{
|
||||
var sig = new byte[64];
|
||||
kp.Sign(nonce, sig);
|
||||
return Convert.ToBase64String(sig).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Valid_bearer_jwt_returns_auth_result()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "test-nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
result.AccountName.ShouldBe(accountPub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Valid_jwt_with_nonce_signature_returns_auth_result()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var nonce = "test-nonce-data"u8.ToArray();
|
||||
var sig = SignNonce(userKp, nonce);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt, Nkey = userPub, Sig = sig },
|
||||
Nonce = nonce,
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
result.AccountName.ShouldBe(accountPub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void No_jwt_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var resolver = new MemAccountResolver();
|
||||
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions(),
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Non_jwt_string_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var resolver = new MemAccountResolver();
|
||||
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = "not-a-jwt" },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Expired_jwt_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// Expired in 2020
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1500000000,
|
||||
"exp":1600000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Revoked_user_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
// Account JWT with revocation for user
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"account","version":2,
|
||||
"revocations":{
|
||||
"{{userPub}}":1700000001
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT issued at 1700000000 (before revocation time 1700000001)
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Untrusted_operator_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
// Use a different trusted key that doesn't match the operator
|
||||
var otherOperator = KeyPair.CreatePair(PrefixByte.Operator).GetPublicKey();
|
||||
var auth = new JwtAuthenticator([otherOperator], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_account_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
// Don't store the account JWT in the resolver
|
||||
var resolver = new MemAccountResolver();
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Non_bearer_without_sig_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// Non-bearer user JWT
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt }, // No Sig provided
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Jwt_with_permissions_returns_permissions()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"pub":{"allow":["foo.>","bar.*"]}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Permissions.ShouldNotBeNull();
|
||||
result.Permissions.Publish.ShouldNotBeNull();
|
||||
result.Permissions.Publish.Allow.ShouldNotBeNull();
|
||||
result.Permissions.Publish.Allow.ShouldContain("foo.>");
|
||||
result.Permissions.Publish.Allow.ShouldContain("bar.*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Signing_key_based_user_jwt_succeeds()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var signingKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var signingPub = signingKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
// Account JWT with signing key
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"account","version":2,
|
||||
"signing_keys":["{{signingPub}}"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT issued by the signing key
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{signingPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, signingKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
result.AccountName.ShouldBe(accountPub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Wildcard_revocation_returns_null()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
// Account JWT with wildcard revocation
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"account","version":2,
|
||||
"revocations":{
|
||||
"*":1700000001
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT issued at 1700000000 (before wildcard revocation)
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
};
|
||||
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// allowed_connection_types tests
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public async Task Allowed_connection_types_allows_standard_context()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"allowed_connection_types":["STANDARD"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
ConnectionType = "STANDARD",
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Allowed_connection_types_rejects_mqtt_only_for_standard_context()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT only allows MQTT connections
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"allowed_connection_types":["MQTT"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
ConnectionType = "STANDARD",
|
||||
};
|
||||
|
||||
// Should reject: STANDARD is not in allowed_connection_types
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Allowed_connection_types_allows_known_even_with_unknown_values()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT allows STANDARD and an unknown type
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"allowed_connection_types":["STANDARD","SOME_NEW_TYPE"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
ConnectionType = "STANDARD",
|
||||
};
|
||||
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Allowed_connection_types_rejects_when_only_unknown_values_present()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT only allows an unknown connection type
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"allowed_connection_types":["SOME_NEW_TYPE"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
ConnectionType = "STANDARD",
|
||||
};
|
||||
|
||||
// Should reject: STANDARD is not in allowed_connection_types
|
||||
auth.Authenticate(ctx).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Allowed_connection_types_is_case_insensitive_for_input_values()
|
||||
{
|
||||
var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
|
||||
var accountKp = KeyPair.CreatePair(PrefixByte.Account);
|
||||
var userKp = KeyPair.CreatePair(PrefixByte.User);
|
||||
|
||||
var operatorPub = operatorKp.GetPublicKey();
|
||||
var accountPub = accountKp.GetPublicKey();
|
||||
var userPub = userKp.GetPublicKey();
|
||||
|
||||
var accountPayload = $$"""
|
||||
{
|
||||
"sub":"{{accountPub}}",
|
||||
"iss":"{{operatorPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{"type":"account","version":2}
|
||||
}
|
||||
""";
|
||||
var accountJwt = BuildSignedToken(accountPayload, operatorKp);
|
||||
|
||||
// User JWT allows "standard" (lowercase)
|
||||
var userPayload = $$"""
|
||||
{
|
||||
"sub":"{{userPub}}",
|
||||
"iss":"{{accountPub}}",
|
||||
"iat":1700000000,
|
||||
"nats":{
|
||||
"type":"user","version":2,
|
||||
"bearer_token":true,
|
||||
"issuer_account":"{{accountPub}}",
|
||||
"allowed_connection_types":["standard"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
var userJwt = BuildSignedToken(userPayload, accountKp);
|
||||
|
||||
var resolver = new MemAccountResolver();
|
||||
await resolver.StoreAsync(accountPub, accountJwt);
|
||||
|
||||
var auth = new JwtAuthenticator([operatorPub], resolver);
|
||||
|
||||
var ctx = new ClientAuthContext
|
||||
{
|
||||
Opts = new ClientOptions { JWT = userJwt },
|
||||
Nonce = "nonce"u8.ToArray(),
|
||||
ConnectionType = "STANDARD",
|
||||
};
|
||||
|
||||
// Should allow: case-insensitive match of "standard" == "STANDARD"
|
||||
var result = auth.Authenticate(ctx);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
result.Identity.ShouldBe(userPub);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user