// 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 { /// /// 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. /// public class JwtGoParityTests { // ========================================================================= // Test helpers — mirror Go's opTrustBasicSetup, buildMemAccResolver, etc. // Reference: jwt_test.go:65-170 // ========================================================================= /// Creates an operator key pair (mirrors Go oKp / nkeys.CreateOperator). private static KeyPair CreateOperatorKp() => KeyPair.CreatePair(PrefixByte.Operator); /// Creates a fresh account key pair (mirrors nkeys.CreateAccount). private static KeyPair CreateAccountKp() => KeyPair.CreatePair(PrefixByte.Account); /// Creates a fresh user key pair (mirrors nkeys.CreateUser). private static KeyPair CreateUserKp() => KeyPair.CreatePair(PrefixByte.User); /// /// 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 /// 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; /// /// Builds an account JWT signed by the operator key pair. /// Reference: jwt_test.go:111-140 setupJWTTestWithClaims /// private static string BuildAccountJwt( KeyPair operatorKp, string accountPub, string[]? signingKeys = null, long? expiresAt = null, long? issuedAt = null, Dictionary? 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); } /// /// Builds a user JWT signed by the account key pair (or a signing key). /// Reference: jwt_test.go:62-109 createClientWithIssuer / createClient /// 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); } /// /// Builds a JwtAuthenticator with the given operator key pair and account JWT pre-loaded. /// Reference: jwt_test.go:137-141 opTrustBasicSetup / buildMemAccResolver / addAccountToMemResolver /// 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); } /// Creates a valid nonce signature for a user key pair. 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); } /// Calls Authenticate with a freshly-signed nonce. 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 { [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 { [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 { ["*"] = 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 { ["*"] = 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 { [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 Valid, bool HasUnknown) Convert(IEnumerable 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? 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