using NATS.Server.Auth.Jwt; namespace NATS.Server.Auth; /// /// Authenticator for JWT-based client connections. /// Decodes user JWT, resolves account, verifies signature, checks revocation. /// Reference: Go auth.go:588+ processClientOrLeafAuthentication. /// public sealed class JwtAuthenticator : IAuthenticator { private readonly string[] _trustedKeys; private readonly IAccountResolver _resolver; public JwtAuthenticator(string[] trustedKeys, IAccountResolver resolver) { _trustedKeys = trustedKeys; _resolver = resolver; } public AuthResult? Authenticate(ClientAuthContext context) { var jwt = context.Opts.JWT; if (string.IsNullOrEmpty(jwt) || !NatsJwt.IsJwt(jwt)) return null; // 1. Decode user claims var userClaims = NatsJwt.DecodeUserClaims(jwt); if (userClaims is null) return null; // 2. Check expiry if (userClaims.IsExpired()) return null; // 3. Resolve issuing account var issuerAccount = !string.IsNullOrEmpty(userClaims.IssuerAccount) ? userClaims.IssuerAccount : userClaims.Issuer; if (string.IsNullOrEmpty(issuerAccount)) return null; var accountJwt = _resolver.FetchAsync(issuerAccount).GetAwaiter().GetResult(); if (accountJwt is null) return null; var accountClaims = NatsJwt.DecodeAccountClaims(accountJwt); if (accountClaims is null) return null; // 4. Verify account issuer is trusted if (!IsTrusted(accountClaims.Issuer)) return null; // 5. Verify user JWT issuer is the account or a signing key var userIssuer = userClaims.Issuer; if (userIssuer != accountClaims.Subject) { // Check if issuer is a signing key of the account var signingKeys = accountClaims.Nats?.SigningKeys; if (signingKeys is null || !signingKeys.Contains(userIssuer)) return null; } // 6. Verify nonce signature (unless bearer token) if (!userClaims.BearerToken) { if (context.Nonce is null || string.IsNullOrEmpty(context.Opts.Sig)) return null; var userNkey = userClaims.Subject ?? context.Opts.Nkey; if (string.IsNullOrEmpty(userNkey)) return null; if (!NatsJwt.VerifyNonce(context.Nonce, context.Opts.Sig, userNkey)) return null; } // 7. Check user revocation var revocations = accountClaims.Nats?.Revocations; if (revocations is not null && userClaims.Subject is not null) { if (revocations.TryGetValue(userClaims.Subject, out var revokedAt)) { if (userClaims.IssuedAt <= revokedAt) return null; } // Check wildcard revocation if (revocations.TryGetValue("*", out revokedAt)) { if (userClaims.IssuedAt <= revokedAt) return null; } } // 7b. Check allowed connection types var (allowedTypes, hasUnknown) = JwtConnectionTypes.Convert(userClaims.Nats?.AllowedConnectionTypes); if (allowedTypes.Count == 0) { if (hasUnknown) return null; // unknown-only list should reject } else { var connType = string.IsNullOrWhiteSpace(context.ConnectionType) ? JwtConnectionTypes.Standard : context.ConnectionType.ToUpperInvariant(); if (!allowedTypes.Contains(connType)) return null; } // 8. Build permissions from JWT claims Permissions? permissions = null; var nats = userClaims.Nats; if (nats is not null) { var pubAllow = nats.Pub?.Allow; var pubDeny = nats.Pub?.Deny; var subAllow = nats.Sub?.Allow; var subDeny = nats.Sub?.Deny; // Expand permission templates var name = userClaims.Name ?? ""; var subject = userClaims.Subject ?? ""; var acctName = accountClaims.Name ?? ""; var acctSubject = accountClaims.Subject ?? ""; var userTags = nats.Tags ?? []; var acctTags = accountClaims.Nats?.Tags ?? []; if (pubAllow is { Length: > 0 }) pubAllow = PermissionTemplates.ExpandAll(pubAllow, name, subject, acctName, acctSubject, userTags, acctTags).ToArray(); if (pubDeny is { Length: > 0 }) pubDeny = PermissionTemplates.ExpandAll(pubDeny, name, subject, acctName, acctSubject, userTags, acctTags).ToArray(); if (subAllow is { Length: > 0 }) subAllow = PermissionTemplates.ExpandAll(subAllow, name, subject, acctName, acctSubject, userTags, acctTags).ToArray(); if (subDeny is { Length: > 0 }) subDeny = PermissionTemplates.ExpandAll(subDeny, name, subject, acctName, acctSubject, userTags, acctTags).ToArray(); if (pubAllow is not null || pubDeny is not null || subAllow is not null || subDeny is not null) { permissions = new Permissions { Publish = (pubAllow is not null || pubDeny is not null) ? new SubjectPermission { Allow = pubAllow, Deny = pubDeny } : null, Subscribe = (subAllow is not null || subDeny is not null) ? new SubjectPermission { Allow = subAllow, Deny = subDeny } : null, }; } } // 9. Build result return new AuthResult { Identity = userClaims.Subject ?? "", AccountName = issuerAccount, Permissions = permissions, Expiry = userClaims.GetExpiry(), MaxJetStreamStreams = accountClaims.Nats?.JetStream?.MaxStreams ?? 0, JetStreamTier = accountClaims.Nats?.JetStream?.Tier, }; } private bool IsTrusted(string? issuer) { if (string.IsNullOrEmpty(issuer)) return false; foreach (var key in _trustedKeys) { if (key == issuer) return true; } return false; } }