test: add failing jwt allowed connection type coverage
Add 5 tests for JWT allowed_connection_types enforcement which the authenticator does not yet implement. Two tests (reject MQTT-only for STANDARD context, reject unknown-only types) fail on assertions because JwtAuthenticator currently ignores the claim. Three tests (allow STANDARD, allow with unknown mixed in, case-insensitive match) pass trivially since the field is not checked. Also adds ConnectionType property to ClientAuthContext (defaults to "STANDARD") so the tests compile.
This commit is contained in:
@@ -13,4 +13,11 @@ public sealed class ClientAuthContext
|
|||||||
public required ClientOptions Opts { get; init; }
|
public required ClientOptions Opts { get; init; }
|
||||||
public required byte[] Nonce { get; init; }
|
public required byte[] Nonce { get; init; }
|
||||||
public X509Certificate2? ClientCertificate { get; init; }
|
public X509Certificate2? ClientCertificate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of connection (e.g., "STANDARD", "WEBSOCKET", "MQTT", "LEAFNODE").
|
||||||
|
/// Used by JWT authenticator to enforce allowed_connection_types claims.
|
||||||
|
/// Defaults to "STANDARD" for regular NATS client connections.
|
||||||
|
/// </summary>
|
||||||
|
public string ConnectionType { get; init; } = "STANDARD";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,4 +588,279 @@ public class JwtAuthenticatorTests
|
|||||||
|
|
||||||
auth.Authenticate(ctx).ShouldBeNull();
|
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