Files
natsdotnet/tests/NATS.Server.Auth.Tests/JwtAuthenticatorTests.cs
Joseph Doherty 36b9dfa654 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.
2026-03-12 15:54:07 -04:00

867 lines
27 KiB
C#

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);
}
}