75 tests covering JWT token parsing, account claims, NKey authentication, authorization callout, user JWT validation, and account resolver patterns. Go refs: jwt_test.go, nkey_test.go, accounts_test.go
1771 lines
67 KiB
C#
1771 lines
67 KiB
C#
// Port of Go server/jwt_test.go — JWT claims, trust chain, signing keys, revocation,
|
|
// bearer tokens, permission templates, connection type filtering, and account limits.
|
|
// Reference: golang/nats-server/server/jwt_test.go
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using NATS.NKeys;
|
|
using NATS.Server.Auth;
|
|
using NATS.Server.Auth.Jwt;
|
|
using NATS.Server.Protocol;
|
|
|
|
namespace NATS.Server.Tests.Auth
|
|
{
|
|
|
|
/// <summary>
|
|
/// Parity tests ported from Go server/jwt_test.go covering JWT authentication:
|
|
/// trust chain validation, signing key hierarchy, expiration, revocation,
|
|
/// bearer tokens, permission claims, account limits, connection type filtering,
|
|
/// and permission template expansion.
|
|
/// </summary>
|
|
public class JwtGoParityTests
|
|
{
|
|
// =========================================================================
|
|
// Test helpers — mirror Go's opTrustBasicSetup, buildMemAccResolver, etc.
|
|
// Reference: jwt_test.go:65-170
|
|
// =========================================================================
|
|
|
|
/// <summary>Creates an operator key pair (mirrors Go oKp / nkeys.CreateOperator).</summary>
|
|
private static KeyPair CreateOperatorKp()
|
|
=> KeyPair.CreatePair(PrefixByte.Operator);
|
|
|
|
/// <summary>Creates a fresh account key pair (mirrors nkeys.CreateAccount).</summary>
|
|
private static KeyPair CreateAccountKp()
|
|
=> KeyPair.CreatePair(PrefixByte.Account);
|
|
|
|
/// <summary>Creates a fresh user key pair (mirrors nkeys.CreateUser).</summary>
|
|
private static KeyPair CreateUserKp()
|
|
=> KeyPair.CreatePair(PrefixByte.User);
|
|
|
|
/// <summary>
|
|
/// Encodes a JWT payload using Ed25519 signing with the given key pair.
|
|
/// Matches the NATS JWT wire format: base64url(header).base64url(payload).base64url(signature).
|
|
/// Reference: github.com/nats-io/jwt/v2 — Claims.Encode
|
|
/// </summary>
|
|
private static string EncodeJwt(object payload, KeyPair signingKp)
|
|
{
|
|
var header = new JwtEncodingHeader { Algorithm = "ed25519-nkey", Type = "jwt" };
|
|
var headerJson = JsonSerializer.Serialize(header);
|
|
var payloadJson = JsonSerializer.Serialize(payload);
|
|
|
|
var headerB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson));
|
|
var payloadB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));
|
|
|
|
var signingInput = $"{headerB64}.{payloadB64}";
|
|
var signingInputBytes = Encoding.UTF8.GetBytes(signingInput);
|
|
|
|
var sig = new byte[64];
|
|
signingKp.Sign(signingInputBytes, sig);
|
|
|
|
var sigB64 = Base64UrlEncode(sig);
|
|
return $"{headerB64}.{payloadB64}.{sigB64}";
|
|
}
|
|
|
|
private static string Base64UrlEncode(byte[] data)
|
|
=> Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
|
|
private static long Now() => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
private static long Ago(int seconds) => Now() - seconds;
|
|
private static long InFuture(int seconds) => Now() + seconds;
|
|
|
|
/// <summary>
|
|
/// Builds an account JWT signed by the operator key pair.
|
|
/// Reference: jwt_test.go:111-140 setupJWTTestWithClaims
|
|
/// </summary>
|
|
private static string BuildAccountJwt(
|
|
KeyPair operatorKp,
|
|
string accountPub,
|
|
string[]? signingKeys = null,
|
|
long? expiresAt = null,
|
|
long? issuedAt = null,
|
|
Dictionary<string, long>? revocations = null,
|
|
JwtTestAccountLimits? limits = null,
|
|
string? name = null,
|
|
string[]? tags = null)
|
|
{
|
|
var operatorPub = operatorKp.GetPublicKey();
|
|
var payload = new JwtTestAccountClaims
|
|
{
|
|
Subject = accountPub,
|
|
Issuer = operatorPub,
|
|
IssuedAt = issuedAt ?? Now(),
|
|
Expires = expiresAt ?? 0,
|
|
Name = name,
|
|
Nats = new JwtTestAccountNats
|
|
{
|
|
Type = "account",
|
|
Version = 2,
|
|
SigningKeys = signingKeys,
|
|
Revocations = revocations,
|
|
Limits = limits,
|
|
Tags = tags,
|
|
},
|
|
};
|
|
return EncodeJwt(payload, operatorKp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a user JWT signed by the account key pair (or a signing key).
|
|
/// Reference: jwt_test.go:62-109 createClientWithIssuer / createClient
|
|
/// </summary>
|
|
private static string BuildUserJwt(
|
|
KeyPair signingKp,
|
|
string userPub,
|
|
string? issuerAccount = null,
|
|
long? expiresAt = null,
|
|
long? issuedAt = null,
|
|
bool bearerToken = false,
|
|
string[]? pubAllow = null,
|
|
string[]? pubDeny = null,
|
|
string[]? subAllow = null,
|
|
string[]? subDeny = null,
|
|
string[]? allowedConnectionTypes = null,
|
|
string? name = null,
|
|
string[]? tags = null)
|
|
{
|
|
var signingPub = signingKp.GetPublicKey();
|
|
var payload = new JwtTestUserClaims
|
|
{
|
|
Subject = userPub,
|
|
Issuer = signingPub,
|
|
IssuedAt = issuedAt ?? Now(),
|
|
Expires = expiresAt ?? 0,
|
|
Name = name,
|
|
Nats = new JwtTestUserNats
|
|
{
|
|
Type = "user",
|
|
Version = 2,
|
|
IssuerAccount = issuerAccount,
|
|
BearerToken = bearerToken ? true : null,
|
|
AllowedConnectionTypes = allowedConnectionTypes,
|
|
Tags = tags,
|
|
Pub = (pubAllow != null || pubDeny != null)
|
|
? new JwtTestSubjectPerm { Allow = pubAllow, Deny = pubDeny }
|
|
: null,
|
|
Sub = (subAllow != null || subDeny != null)
|
|
? new JwtTestSubjectPerm { Allow = subAllow, Deny = subDeny }
|
|
: null,
|
|
},
|
|
};
|
|
return EncodeJwt(payload, signingKp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a JwtAuthenticator with the given operator key pair and account JWT pre-loaded.
|
|
/// Reference: jwt_test.go:137-141 opTrustBasicSetup / buildMemAccResolver / addAccountToMemResolver
|
|
/// </summary>
|
|
private static (JwtAuthenticator auth, MemAccountResolver resolver) BuildAuthenticator(
|
|
KeyPair operatorKp,
|
|
string? accountPub = null,
|
|
string? accountJwt = null)
|
|
{
|
|
var resolver = new MemAccountResolver();
|
|
if (accountPub != null && accountJwt != null)
|
|
resolver.StoreAsync(accountPub, accountJwt).GetAwaiter().GetResult();
|
|
|
|
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
|
return (auth, resolver);
|
|
}
|
|
|
|
/// <summary>Creates a valid nonce signature for a user key pair.</summary>
|
|
private static (byte[] nonce, string sig) CreateNonceSig(KeyPair userKp)
|
|
{
|
|
var nonce = new byte[16];
|
|
RandomNumberGenerator.Fill(nonce);
|
|
var sig = new byte[64];
|
|
userKp.Sign(nonce, sig);
|
|
var sigStr = Base64UrlEncode(sig);
|
|
return (nonce, sigStr);
|
|
}
|
|
|
|
/// <summary>Calls Authenticate with a freshly-signed nonce.</summary>
|
|
private static AuthResult? Authenticate(
|
|
JwtAuthenticator auth,
|
|
string userJwt,
|
|
KeyPair userKp)
|
|
{
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
return auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUser — basic trust chain: operator -> account -> user
|
|
// Go reference: jwt_test.go:199 TestJWTUser
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_NoJwt_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUser — connecting without jwt field should fail (return null).
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions(),
|
|
Nonce = [],
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_NoAccountResolver_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUser — connecting with JWT but no account in resolver should fail.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
// Authenticator with empty resolver
|
|
var resolver = new MemAccountResolver();
|
|
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
|
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_ValidTrustChain_ReturnsResult()
|
|
{
|
|
// Go: TestJWTUser — valid operator -> account -> user chain allows connection.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Identity.ShouldBe(userPub);
|
|
result.AccountName.ShouldBe(accountPub);
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserBadTrusted — bad trusted key rejects valid JWT
|
|
// Go reference: jwt_test.go:255 TestJWTUserBadTrusted
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task JwtUser_BadTrustedKey_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUserBadTrusted — replacing trusted keys with invalid key rejects connections.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
|
|
using var badOperatorKp = CreateOperatorKp();
|
|
var resolver = new MemAccountResolver();
|
|
await resolver.StoreAsync(accountPub, accountJwt);
|
|
var auth = new JwtAuthenticator([badOperatorKp.GetPublicKey()], resolver);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserExpired — expired user JWT
|
|
// Go reference: jwt_test.go:292 TestJWTUserExpired
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_ExpiredUser_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUserExpired — expired user JWT is rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
issuedAt: Ago(10),
|
|
expiresAt: Ago(2));
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserExpiresAfterConnect — JWT not yet expired returns result
|
|
// Go reference: jwt_test.go:301 TestJWTUserExpiresAfterConnect
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_NotYetExpired_ReturnsResult()
|
|
{
|
|
// Go: TestJWTUserExpiresAfterConnect — a JWT that expires in the future should succeed at connect time.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
issuedAt: Now(),
|
|
expiresAt: InFuture(60));
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Expiry.ShouldNotBeNull();
|
|
var expiryUnix = result.Expiry!.Value.ToUnixTimeSeconds();
|
|
(expiryUnix >= InFuture(58) && expiryUnix <= InFuture(62)).ShouldBeTrue("expiry should be approximately 60 seconds in the future");
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTAccountExpired — expired account JWT decode test
|
|
// Go reference: jwt_test.go:473 TestJWTAccountExpired
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAccount_ExpiredClaims_DecodedCorrectly()
|
|
{
|
|
// Go: TestJWTAccountExpired — expired account JWT can still be decoded.
|
|
// The .NET JwtAuthenticator checks user expiry; account expiry is a server-level concern.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
issuedAt: Ago(10),
|
|
expiresAt: Ago(2));
|
|
|
|
var claims = NatsJwt.DecodeAccountClaims(accountJwt);
|
|
claims.ShouldNotBeNull();
|
|
claims.Expires.ShouldBeGreaterThan(0);
|
|
(DateTimeOffset.UtcNow.ToUnixTimeSeconds() > claims.Expires).ShouldBeTrue();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserPermissionClaims — user permissions from JWT claims
|
|
// Go reference: jwt_test.go:331 TestJWTUserPermissionClaims
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_PermissionClaims_Transferred()
|
|
{
|
|
// Go: TestJWTUserPermissionClaims — pub/sub allow/deny transferred from JWT to AuthResult.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
pubAllow: ["foo", "bar"],
|
|
pubDeny: ["baz"],
|
|
subAllow: ["foo", "bar"],
|
|
subDeny: ["baz"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Permissions.ShouldNotBeNull();
|
|
result.Permissions!.Publish.ShouldNotBeNull();
|
|
result.Permissions!.Subscribe.ShouldNotBeNull();
|
|
|
|
result.Permissions.Publish!.Allow.ShouldNotBeNull();
|
|
result.Permissions.Publish.Allow!.Count.ShouldBe(2);
|
|
result.Permissions.Publish.Deny.ShouldNotBeNull();
|
|
result.Permissions.Publish.Deny!.Count.ShouldBe(1);
|
|
|
|
result.Permissions.Subscribe!.Allow.ShouldNotBeNull();
|
|
result.Permissions.Subscribe.Allow!.Count.ShouldBe(2);
|
|
result.Permissions.Subscribe.Deny.ShouldNotBeNull();
|
|
result.Permissions.Subscribe.Deny!.Count.ShouldBe(1);
|
|
|
|
result.Permissions.Publish.Allow.ShouldContain("foo");
|
|
result.Permissions.Publish.Allow.ShouldContain("bar");
|
|
result.Permissions.Publish.Deny.ShouldContain("baz");
|
|
result.Permissions.Subscribe.Allow.ShouldContain("foo");
|
|
result.Permissions.Subscribe.Allow.ShouldContain("bar");
|
|
result.Permissions.Subscribe.Deny.ShouldContain("baz");
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_NoPermissions_NullPermissions()
|
|
{
|
|
// Go: user with no permissions results in null Permissions on AuthResult.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Permissions.ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserSigningKey — account signing key hierarchy
|
|
// Go reference: jwt_test.go:2241 TestJWTUserSigningKey
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_SigningKey_NotInAccount_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUserSigningKey — signing key not registered in account JWT is rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_SigningKey_RegisteredInAccount_ReturnsResult()
|
|
{
|
|
// Go: TestJWTUserSigningKey — signing key listed in account JWT allows user connection.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var signingPub = signingKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
|
|
var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Identity.ShouldBe(userPub);
|
|
result.AccountName.ShouldBe(accountPub);
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_MultipleSigningKeys_AnyWorks()
|
|
{
|
|
// Go: account can have multiple signing keys; any one can sign user JWTs.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp1 = CreateAccountKp();
|
|
using var signingKp2 = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
signingKeys: [signingKp1.GetPublicKey(), signingKp2.GetPublicKey()]);
|
|
var userJwt = BuildUserJwt(signingKp2, userPub, issuerAccount: accountPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Identity.ShouldBe(userPub);
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTUserRevoked — revoked user JWT
|
|
// Go reference: jwt_test.go:2673 TestJWTUserRevoked
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtUser_Revoked_ReturnsNull()
|
|
{
|
|
// Go: TestJWTUserRevoked — user revoked in account JWT is rejected at connect.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
// Account with user revoked at "now"; user JWT issued 5 seconds ago
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
revocations: new Dictionary<string, long> { [userPub] = Now() });
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Ago(5));
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_NotRevoked_ReturnsResult()
|
|
{
|
|
// Go: user issued AFTER revocation time is not revoked.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
// Revocation set to 10 seconds ago
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
revocations: new Dictionary<string, long> { [userPub] = Ago(10) });
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Now());
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_WildcardRevocation_RevokesAll()
|
|
{
|
|
// Go: TestJWTUserRevocation — wildcard "*" revocation revokes all users issued before the time.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
revocations: new Dictionary<string, long> { ["*"] = Now() });
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Ago(5));
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtUser_WildcardRevocation_NewJwt_NotRevoked()
|
|
{
|
|
// Go: TestJWTUserRevocation — user JWT issued after wildcard revocation time is allowed.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
revocations: new Dictionary<string, long> { ["*"] = Ago(10) });
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuedAt: Now());
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTBearerToken — bearer token skips nonce signature
|
|
// Go reference: jwt_test.go:2997 TestJWTBearerToken
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtBearerToken_NoSignature_ReturnsResult()
|
|
{
|
|
// Go: TestJWTBearerToken — bearer token does not require nonce signature.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, bearerToken: true);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt },
|
|
Nonce = [],
|
|
});
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Identity.ShouldBe(userPub);
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtBearerToken_WithIssuerAccount_SameAsAccount_Succeeds()
|
|
{
|
|
// Go: TestJWTBearerWithIssuerSameAsAccountToken — bearer with IssuerAccount = account pub is OK.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuerAccount: accountPub, bearerToken: true);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt },
|
|
Nonce = [],
|
|
});
|
|
|
|
result.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtBearerToken_WithBadIssuerAccount_ReturnsNull()
|
|
{
|
|
// Go: TestJWTBearerWithBadIssuerToken — bearer with IssuerAccount pointing to different account is rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var differentAccountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var differentAccountPub = differentAccountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, issuerAccount: differentAccountPub, bearerToken: true);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt },
|
|
Nonce = [],
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtNonBearer_NoSignature_ReturnsNull()
|
|
{
|
|
// Go: non-bearer tokens without nonce signature are rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt },
|
|
Nonce = [],
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtNonBearer_BadSignature_ReturnsNull()
|
|
{
|
|
// Go: non-bearer tokens with incorrect nonce signature are rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
using var wrongKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var nonce = new byte[16];
|
|
RandomNumberGenerator.Fill(nonce);
|
|
var (_, wrongSig) = CreateNonceSig(wrongKp);
|
|
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = wrongSig },
|
|
Nonce = nonce,
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// TestJWTNoOperatorMode — non-operator mode
|
|
// Go reference: jwt_test.go:4483 TestJWTNoOperatorMode
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_NoTrustedKeys_AuthNotRequired()
|
|
{
|
|
// Go: TestJWTNoOperatorMode — without operator mode, auth is not required (no TrustedKeys).
|
|
var service = AuthService.Build(new NatsOptions());
|
|
|
|
service.IsAuthRequired.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtAuth_TrustedKeysConfigured_AuthRequired()
|
|
{
|
|
// Go: TestJWTNoOperatorMode — operator mode with TrustedKeys sets auth required.
|
|
using var operatorKp = CreateOperatorKp();
|
|
var service = AuthService.Build(new NatsOptions
|
|
{
|
|
TrustedKeys = [operatorKp.GetPublicKey()],
|
|
AccountResolver = new MemAccountResolver(),
|
|
});
|
|
|
|
service.IsAuthRequired.ShouldBeTrue();
|
|
service.NonceRequired.ShouldBeTrue();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Connection types filtering
|
|
// Go reference: jwt_test.go — allowed_connection_types claim enforcement
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypes_StandardAllowed_Succeeds()
|
|
{
|
|
// Go: when allowed_connection_types includes STANDARD, standard client connects.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["STANDARD"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
ConnectionType = "STANDARD",
|
|
});
|
|
|
|
result.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypes_WebsocketOnly_StandardRejected()
|
|
{
|
|
// Go: when allowed_connection_types only includes WEBSOCKET, standard client is rejected.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["WEBSOCKET"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
ConnectionType = "STANDARD",
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypes_EmptyList_AllConnectionsAllowed()
|
|
{
|
|
// Go: empty allowed_connection_types means all types are allowed.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
ConnectionType = "STANDARD",
|
|
});
|
|
|
|
result.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypes_MultipleTypes_AnyMatchingAllowed()
|
|
{
|
|
// Go: multiple allowed connection types — any matching type is allowed.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
allowedConnectionTypes: ["STANDARD", "WEBSOCKET", "LEAFNODE"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
foreach (var connType in new[] { "STANDARD", "WEBSOCKET", "LEAFNODE" })
|
|
{
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
ConnectionType = connType,
|
|
});
|
|
result.ShouldNotBeNull($"expected connection type {connType} to be allowed");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypes_UnknownTypeOnly_ReturnsNull()
|
|
{
|
|
// Go: unknown connection type values cause rejection.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub, allowedConnectionTypes: ["UNKNOWN_TYPE_XYZ"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var (nonce, sig) = CreateNonceSig(userKp);
|
|
var result = auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = userJwt, Sig = sig },
|
|
Nonce = nonce,
|
|
ConnectionType = "STANDARD",
|
|
});
|
|
|
|
result.ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.IsJwt — JWT string detection
|
|
// Go reference: jwt_test.go:4951 TestJWTHeader
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_IsJwt_ValidJwt_ReturnsTrue()
|
|
{
|
|
// Go: TestJWTHeader — valid JWT starts with "eyJ".
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());
|
|
|
|
NatsJwt.IsJwt(userJwt).ShouldBeTrue();
|
|
userJwt.ShouldStartWith("eyJ");
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_IsJwt_EmptyString_ReturnsFalse()
|
|
{
|
|
NatsJwt.IsJwt("").ShouldBeFalse();
|
|
NatsJwt.IsJwt(null!).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_IsJwt_PlainString_ReturnsFalse()
|
|
{
|
|
NatsJwt.IsJwt("not-a-jwt").ShouldBeFalse();
|
|
NatsJwt.IsJwt("Bearer token").ShouldBeFalse();
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.Decode — structural JWT parsing
|
|
// Go reference: jwt_test.go:4951 TestJWTHeader
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_Decode_ValidToken_ReturnsToken()
|
|
{
|
|
// Go: TestJWTHeader — JWT decoded into header+payload+signature.
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());
|
|
|
|
var decoded = NatsJwt.Decode(userJwt);
|
|
|
|
decoded.ShouldNotBeNull();
|
|
decoded.Header.ShouldNotBeNull();
|
|
decoded.Header.Algorithm.ShouldBe("ed25519-nkey");
|
|
decoded.Header.Type.ShouldBe("jwt");
|
|
decoded.PayloadJson.ShouldNotBeNullOrEmpty();
|
|
decoded.Signature.ShouldNotBeNull();
|
|
decoded.Signature.Length.ShouldBe(64);
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_Decode_MalformedToken_ReturnsNull()
|
|
{
|
|
NatsJwt.Decode("not.valid").ShouldBeNull();
|
|
NatsJwt.Decode("").ShouldBeNull();
|
|
NatsJwt.Decode("a.b.c.d").ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.DecodeUserClaims — user claims deserialization
|
|
// Go reference: jwt_test.go — user claims structures
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_DecodeUserClaims_AllFields_Populated()
|
|
{
|
|
// Go: user claims decoded from JWT with all standard fields.
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
var iat = Now();
|
|
var exp = InFuture(3600);
|
|
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
issuedAt: iat,
|
|
expiresAt: exp,
|
|
name: "testuser",
|
|
pubAllow: ["foo", "bar"]);
|
|
|
|
var claims = NatsJwt.DecodeUserClaims(userJwt);
|
|
|
|
claims.ShouldNotBeNull();
|
|
claims.Subject.ShouldBe(userPub);
|
|
claims.Issuer.ShouldBe(accountPub);
|
|
claims.IssuedAt.ShouldBe(iat);
|
|
claims.Expires.ShouldBe(exp);
|
|
claims.Name.ShouldBe("testuser");
|
|
claims.Nats.ShouldNotBeNull();
|
|
claims.Nats!.Pub.ShouldNotBeNull();
|
|
claims.Nats.Pub!.Allow.ShouldNotBeNull();
|
|
claims.Nats.Pub.Allow!.Length.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_DecodeUserClaims_BearerToken_Parsed()
|
|
{
|
|
// Go: TestJWTBearerToken — bearer_token field parsed correctly.
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey(), bearerToken: true);
|
|
var claims = NatsJwt.DecodeUserClaims(userJwt);
|
|
|
|
claims.ShouldNotBeNull();
|
|
claims.BearerToken.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_DecodeUserClaims_IssuerAccount_Parsed()
|
|
{
|
|
// Go: TestJWTUserSigningKey — issuer_account field preserved in claims.
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
|
|
var userJwt = BuildUserJwt(signingKp, userKp.GetPublicKey(), issuerAccount: accountPub);
|
|
var claims = NatsJwt.DecodeUserClaims(userJwt);
|
|
|
|
claims.ShouldNotBeNull();
|
|
claims.IssuerAccount.ShouldBe(accountPub);
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.DecodeAccountClaims — account claims deserialization
|
|
// Go reference: jwt_test.go:629 TestJWTAccountBasicImportExport
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_DecodeAccountClaims_AllFields_Populated()
|
|
{
|
|
// Go: TestJWTAccountBasicImportExport — account claims decoded with signing keys and revocations.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var signingPub = signingKp.GetPublicKey();
|
|
var revokedUser = "UREVOKED123";
|
|
var revocationTime = Ago(100);
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
signingKeys: [signingPub],
|
|
revocations: new Dictionary<string, long> { [revokedUser] = revocationTime },
|
|
name: "TestAccount");
|
|
|
|
var claims = NatsJwt.DecodeAccountClaims(accountJwt);
|
|
|
|
claims.ShouldNotBeNull();
|
|
claims.Subject.ShouldBe(accountPub);
|
|
claims.Issuer.ShouldBe(operatorKp.GetPublicKey());
|
|
claims.Name.ShouldBe("TestAccount");
|
|
claims.Nats.ShouldNotBeNull();
|
|
claims.Nats!.SigningKeys.ShouldNotBeNull();
|
|
claims.Nats.SigningKeys!.ShouldContain(signingPub);
|
|
claims.Nats.Revocations.ShouldNotBeNull();
|
|
claims.Nats.Revocations!.ShouldContainKey(revokedUser);
|
|
claims.Nats.Revocations[revokedUser].ShouldBe(revocationTime);
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.Verify — JWT signature verification
|
|
// Go reference: jwt_test.go — trust chain verification
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_Verify_ValidSignature_ReturnsTrue()
|
|
{
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());
|
|
|
|
NatsJwt.Verify(userJwt, accountPub).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_Verify_WrongKey_ReturnsFalse()
|
|
{
|
|
using var accountKp = CreateAccountKp();
|
|
using var wrongKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());
|
|
|
|
NatsJwt.Verify(userJwt, wrongKp.GetPublicKey()).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_Verify_TamperedPayload_ReturnsFalse()
|
|
{
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userJwt = BuildUserJwt(accountKp, userKp.GetPublicKey());
|
|
|
|
var parts = userJwt.Split('.');
|
|
var tamperedJwt = $"{parts[0]}.{parts[1]}X.{parts[2]}";
|
|
|
|
NatsJwt.Verify(tamperedJwt, accountPub).ShouldBeFalse();
|
|
}
|
|
|
|
// =========================================================================
|
|
// MemAccountResolver — in-memory resolver store/fetch
|
|
// Go reference: jwt_test.go:65-90 buildMemAccResolver / addAccountToMemResolver
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task MemAccountResolver_StoreAndFetch_ReturnsJwt()
|
|
{
|
|
var resolver = new MemAccountResolver();
|
|
resolver.IsReadOnly.ShouldBeFalse();
|
|
|
|
await resolver.StoreAsync("ABCDEF", "jwt-value");
|
|
var jwt = await resolver.FetchAsync("ABCDEF");
|
|
|
|
jwt.ShouldBe("jwt-value");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MemAccountResolver_FetchUnknown_ReturnsNull()
|
|
{
|
|
var resolver = new MemAccountResolver();
|
|
var jwt = await resolver.FetchAsync("NONEXISTENT");
|
|
|
|
jwt.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MemAccountResolver_UpdateExisting_ReplacesJwt()
|
|
{
|
|
var resolver = new MemAccountResolver();
|
|
await resolver.StoreAsync("ACCT1", "old-jwt");
|
|
await resolver.StoreAsync("ACCT1", "new-jwt");
|
|
|
|
var jwt = await resolver.FetchAsync("ACCT1");
|
|
jwt.ShouldBe("new-jwt");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MemAccountResolver_MultipleAccounts_IndependentStorage()
|
|
{
|
|
var resolver = new MemAccountResolver();
|
|
await resolver.StoreAsync("ACCT1", "jwt-for-acct1");
|
|
await resolver.StoreAsync("ACCT2", "jwt-for-acct2");
|
|
|
|
(await resolver.FetchAsync("ACCT1")).ShouldBe("jwt-for-acct1");
|
|
(await resolver.FetchAsync("ACCT2")).ShouldBe("jwt-for-acct2");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Permission templates
|
|
// Go reference: jwt_test.go:4315 TestJWTTemplates
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_NameSubjectAccountExpansion()
|
|
{
|
|
// Go: TestJWTTemplates — name/subject/account-name/account-subject expand correctly.
|
|
var result = PermissionTemplates.Expand(
|
|
"foo.{{name()}}.{{subject()}}.{{account-name()}}.{{account-subject()}}.bar",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: [], accountTags: []);
|
|
|
|
result.Count.ShouldBe(1);
|
|
result[0].ShouldBe("foo.myname.UABC123.accname.AABC456.bar");
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_TagExpansion_SingleTag()
|
|
{
|
|
// Go: TestJWTInLineTemplates — {{tag(bucket)}} expands to tag value.
|
|
var result = PermissionTemplates.Expand(
|
|
"$JS.API.STREAM.INFO.KV_{{tag(bucket)}}",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: ["bucket:a"], accountTags: []);
|
|
|
|
result.Count.ShouldBe(1);
|
|
result[0].ShouldBe("$JS.API.STREAM.INFO.KV_a");
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_TagExpansion_MultipleValues_CartesianProduct()
|
|
{
|
|
// Go: TestJWTTemplates — multiple tag values produce cartesian product.
|
|
var result = PermissionTemplates.Expand(
|
|
"{{tag(foo)}}.none.{{tag(bar)}}",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: ["foo:foo1", "foo:foo2", "bar:bar1", "bar:bar2", "bar:bar3"],
|
|
accountTags: []);
|
|
|
|
result.Count.ShouldBe(6);
|
|
result.ShouldContain("foo1.none.bar1");
|
|
result.ShouldContain("foo2.none.bar3");
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_AccountTagExpansion()
|
|
{
|
|
// Go: TestJWTTemplates — {{account-tag(acc)}} uses account tags.
|
|
var result = PermissionTemplates.Expand(
|
|
"{{tag(foo)}}.{{account-tag(acc)}}",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: ["foo:foo1", "foo:foo2"],
|
|
accountTags: ["acc:acc1", "acc:acc2"]);
|
|
|
|
result.Count.ShouldBe(4);
|
|
result.ShouldContain("foo1.acc1");
|
|
result.ShouldContain("foo2.acc2");
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_MissingTag_EmptyResult()
|
|
{
|
|
// Go: TestJWTTemplates — missing tag produces empty result (subject dropped).
|
|
var result = PermissionTemplates.Expand(
|
|
"{{tag(NOT_THERE)}}",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: ["foo:foo1"], accountTags: []);
|
|
|
|
result.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_NoTemplates_ReturnsOriginal()
|
|
{
|
|
var result = PermissionTemplates.Expand(
|
|
"plain.subject.>",
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: [], accountTags: []);
|
|
|
|
result.Count.ShouldBe(1);
|
|
result[0].ShouldBe("plain.subject.>");
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionTemplates_ExpandAll_MultiplePatterns_Flattened()
|
|
{
|
|
// Go: TestJWTTemplates — ExpandAll processes multiple patterns, dropping empties.
|
|
var patterns = new[] { "{{name()}}", "plain.sub", "{{tag(NOT_THERE)}}" };
|
|
var result = PermissionTemplates.ExpandAll(patterns,
|
|
name: "myname", subject: "UABC123",
|
|
accountName: "accname", accountSubject: "AABC456",
|
|
userTags: [], accountTags: []);
|
|
|
|
result.Count.ShouldBe(2);
|
|
result.ShouldContain("myname");
|
|
result.ShouldContain("plain.sub");
|
|
}
|
|
|
|
// =========================================================================
|
|
// JwtConnectionTypes static conversion
|
|
// Go reference: jwt_test.go — allowed_connection_types claim
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypesConvert_KnownTypes_AllRecognized()
|
|
{
|
|
var knownTypes = new[]
|
|
{
|
|
"STANDARD", "WEBSOCKET", "LEAFNODE", "LEAFNODE_WS", "MQTT", "MQTT_WS", "INPROCESS"
|
|
};
|
|
|
|
foreach (var type in knownTypes)
|
|
{
|
|
var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert([type]);
|
|
valid.ShouldContain(type);
|
|
hasUnknown.ShouldBeFalse();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypesConvert_CaseInsensitive_Normalized()
|
|
{
|
|
var (valid, _) = JwtConnectionTypesTestHelper.Convert(["standard", "WebSocket"]);
|
|
valid.ShouldContain("STANDARD");
|
|
valid.ShouldContain("WEBSOCKET");
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypesConvert_UnknownType_HasUnknownFlag()
|
|
{
|
|
var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert(["UNKNOWN_XYZ"]);
|
|
valid.Count.ShouldBe(0);
|
|
hasUnknown.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypesConvert_MixedKnownUnknown_PartialValid()
|
|
{
|
|
var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert(["STANDARD", "UNKNOWN_TYPE"]);
|
|
valid.ShouldContain("STANDARD");
|
|
hasUnknown.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtConnectionTypesConvert_EmptyList_BothEmpty()
|
|
{
|
|
var (valid, hasUnknown) = JwtConnectionTypesTestHelper.Convert([]);
|
|
valid.Count.ShouldBe(0);
|
|
hasUnknown.ShouldBeFalse();
|
|
}
|
|
|
|
// =========================================================================
|
|
// UserClaims helpers
|
|
// Go reference: jwt_test.go:292 TestJWTUserExpired — IsExpired()
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void UserClaims_IsExpired_ZeroExpiry_ReturnsFalse()
|
|
{
|
|
new UserClaims { Expires = 0 }.IsExpired().ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void UserClaims_IsExpired_FutureExpiry_ReturnsFalse()
|
|
{
|
|
new UserClaims { Expires = InFuture(3600) }.IsExpired().ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void UserClaims_IsExpired_PastExpiry_ReturnsTrue()
|
|
{
|
|
new UserClaims { Expires = Ago(10) }.IsExpired().ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void UserClaims_GetExpiry_ZeroExpires_ReturnsNull()
|
|
{
|
|
new UserClaims { Expires = 0 }.GetExpiry().ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void UserClaims_GetExpiry_NonZeroExpires_ReturnsDateTimeOffset()
|
|
{
|
|
var exp = InFuture(3600);
|
|
var expiry = new UserClaims { Expires = exp }.GetExpiry();
|
|
expiry.ShouldNotBeNull();
|
|
expiry!.Value.ToUnixTimeSeconds().ShouldBe(exp);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Trust chain: user issuer validation
|
|
// Go reference: jwt_test.go:2241 TestJWTUserSigningKey
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_UserIssuedByWrongAccount_ReturnsNull()
|
|
{
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var wrongAccountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub);
|
|
var userJwt = BuildUserJwt(wrongAccountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
Authenticate(auth, userJwt, userKp).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task JwtAuth_AccountIssuedByWrongOperator_ReturnsNull()
|
|
{
|
|
using var trustedOpKp = CreateOperatorKp();
|
|
using var untrustedOpKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(untrustedOpKp, accountPub);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
|
|
var resolver = new MemAccountResolver();
|
|
await resolver.StoreAsync(accountPub, accountJwt);
|
|
var auth = new JwtAuthenticator([trustedOpKp.GetPublicKey()], resolver);
|
|
|
|
Authenticate(auth, userJwt, userKp).ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Account limits from JWT claims
|
|
// Go reference: jwt_test.go:1088 TestJWTAccountLimitsSubs
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_AccountLimits_Parsed_FromClaims()
|
|
{
|
|
// Go: TestJWTAccountLimitsSubs — account limits decoded correctly from JWT.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub,
|
|
limits: new JwtTestAccountLimits
|
|
{
|
|
MaxSubscriptions = 10,
|
|
MaxPayload = 8,
|
|
MaxConnections = 5,
|
|
MaxData = 1024 * 1024,
|
|
});
|
|
|
|
var claims = NatsJwt.DecodeAccountClaims(accountJwt);
|
|
claims.ShouldNotBeNull();
|
|
claims.Nats!.Limits.ShouldNotBeNull();
|
|
claims.Nats.Limits!.MaxSubscriptions.ShouldBe(10);
|
|
claims.Nats.Limits.MaxPayload.ShouldBe(8);
|
|
claims.Nats.Limits.MaxConnections.ShouldBe(5);
|
|
claims.Nats.Limits.MaxData.ShouldBe(1024 * 1024);
|
|
}
|
|
|
|
// =========================================================================
|
|
// JetStream limits from JWT
|
|
// Go reference: jwt_test.go:5400 TestJWTJetStreamTiers
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_JetStreamLimits_InAuthResult()
|
|
{
|
|
// Go: TestJWTJetStreamTiers — JetStream max_streams from account JWT in AuthResult.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwtWithJetStream(operatorKp, accountPub, maxStreams: 10);
|
|
var userJwt = BuildUserJwt(accountKp, userPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.MaxJetStreamStreams.ShouldBe(10);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Invalid JWT token edge cases
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_MalformedToken_ReturnsNull()
|
|
{
|
|
using var operatorKp = CreateOperatorKp();
|
|
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], new MemAccountResolver());
|
|
|
|
auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = "not-a-jwt-at-all" },
|
|
Nonce = [],
|
|
}).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtAuth_EmptyJwtField_ReturnsNull()
|
|
{
|
|
using var operatorKp = CreateOperatorKp();
|
|
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], new MemAccountResolver());
|
|
|
|
auth.Authenticate(new ClientAuthContext
|
|
{
|
|
Opts = new ClientOptions { JWT = "" },
|
|
Nonce = [],
|
|
}).ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Signing key removed blocks new connections
|
|
// Go reference: jwt_test.go:2338 TestJWTAccountImportSignerRemoved
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task JwtAuth_SigningKeyRemoved_NewConnectionFails()
|
|
{
|
|
// Go: TestJWTAccountImportSignerRemoved — removing signing key blocks new connections.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var signingPub = signingKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var resolver = new MemAccountResolver();
|
|
|
|
// Step 1: signing key registered — succeeds
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
|
|
await resolver.StoreAsync(accountPub, accountJwt);
|
|
var auth = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
|
var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
|
|
|
|
Authenticate(auth, userJwt, userKp).ShouldNotBeNull();
|
|
|
|
// Step 2: signing key removed — new connection fails
|
|
var accountJwtNoKey = BuildAccountJwt(operatorKp, accountPub, signingKeys: null);
|
|
await resolver.StoreAsync(accountPub, accountJwtNoKey);
|
|
var auth2 = new JwtAuthenticator([operatorKp.GetPublicKey()], resolver);
|
|
|
|
Authenticate(auth2, userJwt, userKp).ShouldBeNull();
|
|
}
|
|
|
|
// =========================================================================
|
|
// AuthResult identity and account name
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_AuthResult_IdentityIsUserPublicKey()
|
|
{
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub,
|
|
BuildAccountJwt(operatorKp, accountPub));
|
|
var result = Authenticate(auth, BuildUserJwt(accountKp, userPub), userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Identity.ShouldBe(userPub);
|
|
result.Identity.ShouldStartWith("U");
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtAuth_AuthResult_AccountNameIsAccountPublicKey()
|
|
{
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub,
|
|
BuildAccountJwt(operatorKp, accountPub));
|
|
var result = Authenticate(auth, BuildUserJwt(accountKp, userPub), userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.AccountName.ShouldBe(accountPub);
|
|
result.AccountName!.ShouldStartWith("A");
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtAuth_AuthResult_WithSigningKey_AccountNameIsAccountPub()
|
|
{
|
|
// Go: TestJWTUserSigningKey — AccountName is account pub even when signing key used.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var signingKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var signingPub = signingKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub, signingKeys: [signingPub]);
|
|
var userJwt = BuildUserJwt(signingKp, userPub, issuerAccount: accountPub);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.AccountName.ShouldBe(accountPub);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Permission template integration in JwtAuthenticator
|
|
// Go reference: jwt_test.go:4315 TestJWTTemplates
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void JwtAuth_PermissionTemplates_Expanded_InAuthResult()
|
|
{
|
|
// Go: TestJWTTemplates — permission templates in JWT are expanded during authentication.
|
|
using var operatorKp = CreateOperatorKp();
|
|
using var accountKp = CreateAccountKp();
|
|
using var userKp = CreateUserKp();
|
|
var accountPub = accountKp.GetPublicKey();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var accountJwt = BuildAccountJwt(operatorKp, accountPub, name: "accname");
|
|
var userJwt = BuildUserJwt(accountKp, userPub,
|
|
name: "myname",
|
|
pubAllow: ["foo.{{name()}}"],
|
|
subAllow: ["bar.{{subject()}}"]);
|
|
var (auth, _) = BuildAuthenticator(operatorKp, accountPub, accountJwt);
|
|
|
|
var result = Authenticate(auth, userJwt, userKp);
|
|
|
|
result.ShouldNotBeNull();
|
|
result.Permissions.ShouldNotBeNull();
|
|
result.Permissions!.Publish!.Allow.ShouldNotBeNull();
|
|
result.Permissions.Publish.Allow!.ShouldContain("foo.myname");
|
|
result.Permissions.Subscribe!.Allow.ShouldNotBeNull();
|
|
result.Permissions.Subscribe.Allow!.ShouldContain($"bar.{userPub}");
|
|
}
|
|
|
|
// =========================================================================
|
|
// NatsJwt.VerifyNonce — nonce signature verification
|
|
// Go reference: jwt_test.go — nonce signing in createClientWithIssuer
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void NatsJwt_VerifyNonce_ValidSignature_ReturnsTrue()
|
|
{
|
|
using var userKp = CreateUserKp();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var nonce = new byte[16];
|
|
RandomNumberGenerator.Fill(nonce);
|
|
var sig = new byte[64];
|
|
userKp.Sign(nonce, sig);
|
|
|
|
NatsJwt.VerifyNonce(nonce, Base64UrlEncode(sig), userPub).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_VerifyNonce_WrongKey_ReturnsFalse()
|
|
{
|
|
using var userKp = CreateUserKp();
|
|
using var wrongKp = CreateUserKp();
|
|
|
|
var nonce = new byte[16];
|
|
RandomNumberGenerator.Fill(nonce);
|
|
var sig = new byte[64];
|
|
userKp.Sign(nonce, sig);
|
|
|
|
NatsJwt.VerifyNonce(nonce, Base64UrlEncode(sig), wrongKp.GetPublicKey()).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void NatsJwt_VerifyNonce_ModifiedNonce_ReturnsFalse()
|
|
{
|
|
using var userKp = CreateUserKp();
|
|
var userPub = userKp.GetPublicKey();
|
|
|
|
var nonce = new byte[16];
|
|
RandomNumberGenerator.Fill(nonce);
|
|
var sig = new byte[64];
|
|
userKp.Sign(nonce, sig);
|
|
var sigStr = Base64UrlEncode(sig);
|
|
|
|
nonce[0] ^= 0xFF; // tamper after signing
|
|
|
|
NatsJwt.VerifyNonce(nonce, sigStr, userPub).ShouldBeFalse();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helper: build account JWT with JetStream limits
|
|
// =========================================================================
|
|
|
|
private static string BuildAccountJwtWithJetStream(
|
|
KeyPair operatorKp,
|
|
string accountPub,
|
|
int maxStreams)
|
|
{
|
|
var payload = new JwtTestAccountClaimsJs
|
|
{
|
|
Subject = accountPub,
|
|
Issuer = operatorKp.GetPublicKey(),
|
|
IssuedAt = Now(),
|
|
Expires = 0,
|
|
Nats = new JwtTestAccountNatsJs
|
|
{
|
|
Type = "account",
|
|
Version = 2,
|
|
JetStream = new JwtTestJetStreamLimits { MaxStreams = maxStreams },
|
|
},
|
|
};
|
|
return EncodeJwt(payload, operatorKp);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Internal helper: expose JwtConnectionTypes.Convert (internal) for testing
|
|
// =========================================================================
|
|
|
|
internal static class JwtConnectionTypesTestHelper
|
|
{
|
|
public static (HashSet<string> Valid, bool HasUnknown) Convert(IEnumerable<string> values)
|
|
=> JwtConnectionTypes.Convert(values);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Internal payload types for JWT encoding in tests.
|
|
// Prefixed "JwtTest" to avoid conflicts with production types.
|
|
// =========================================================================
|
|
|
|
internal sealed class JwtEncodingHeader
|
|
{
|
|
[JsonPropertyName("alg")] public string? Algorithm { get; set; }
|
|
[JsonPropertyName("typ")] public string? Type { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestAccountClaims
|
|
{
|
|
[JsonPropertyName("sub")] public string? Subject { get; set; }
|
|
[JsonPropertyName("iss")] public string? Issuer { get; set; }
|
|
[JsonPropertyName("iat")] public long IssuedAt { get; set; }
|
|
[JsonPropertyName("exp")] public long Expires { get; set; }
|
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
|
[JsonPropertyName("nats")] public JwtTestAccountNats? Nats { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestAccountNats
|
|
{
|
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
|
[JsonPropertyName("version")] public int Version { get; set; }
|
|
[JsonPropertyName("signing_keys")] public string[]? SigningKeys { get; set; }
|
|
[JsonPropertyName("revocations")] public Dictionary<string, long>? Revocations { get; set; }
|
|
[JsonPropertyName("limits")] public JwtTestAccountLimits? Limits { get; set; }
|
|
[JsonPropertyName("tags")] public string[]? Tags { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestAccountLimits
|
|
{
|
|
[JsonPropertyName("conn")] public long MaxConnections { get; set; }
|
|
[JsonPropertyName("subs")] public long MaxSubscriptions { get; set; }
|
|
[JsonPropertyName("payload")] public long MaxPayload { get; set; }
|
|
[JsonPropertyName("data")] public long MaxData { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestAccountClaimsJs
|
|
{
|
|
[JsonPropertyName("sub")] public string? Subject { get; set; }
|
|
[JsonPropertyName("iss")] public string? Issuer { get; set; }
|
|
[JsonPropertyName("iat")] public long IssuedAt { get; set; }
|
|
[JsonPropertyName("exp")] public long Expires { get; set; }
|
|
[JsonPropertyName("nats")] public JwtTestAccountNatsJs? Nats { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestAccountNatsJs
|
|
{
|
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
|
[JsonPropertyName("version")] public int Version { get; set; }
|
|
[JsonPropertyName("jetstream")] public JwtTestJetStreamLimits? JetStream { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestJetStreamLimits
|
|
{
|
|
[JsonPropertyName("max_streams")] public int MaxStreams { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestUserClaims
|
|
{
|
|
[JsonPropertyName("sub")] public string? Subject { get; set; }
|
|
[JsonPropertyName("iss")] public string? Issuer { get; set; }
|
|
[JsonPropertyName("iat")] public long IssuedAt { get; set; }
|
|
[JsonPropertyName("exp")] public long Expires { get; set; }
|
|
[JsonPropertyName("name")] public string? Name { get; set; }
|
|
[JsonPropertyName("nats")] public JwtTestUserNats? Nats { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestUserNats
|
|
{
|
|
[JsonPropertyName("type")] public string? Type { get; set; }
|
|
[JsonPropertyName("version")] public int Version { get; set; }
|
|
[JsonPropertyName("issuer_account")] public string? IssuerAccount { get; set; }
|
|
[JsonPropertyName("bearer_token")]
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public bool? BearerToken { get; set; }
|
|
[JsonPropertyName("allowed_connection_types")] public string[]? AllowedConnectionTypes { get; set; }
|
|
[JsonPropertyName("tags")] public string[]? Tags { get; set; }
|
|
[JsonPropertyName("pub")] public JwtTestSubjectPerm? Pub { get; set; }
|
|
[JsonPropertyName("sub")] public JwtTestSubjectPerm? Sub { get; set; }
|
|
}
|
|
|
|
internal sealed class JwtTestSubjectPerm
|
|
{
|
|
[JsonPropertyName("allow")] public string[]? Allow { get; set; }
|
|
[JsonPropertyName("deny")] public string[]? Deny { get; set; }
|
|
}
|
|
|
|
} // namespace NATS.Server.Tests.Auth
|