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.
867 lines
27 KiB
C#
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);
|
|
}
|
|
}
|