diff --git a/differences.md b/differences.md
index 08eeef1..a0c5fa0 100644
--- a/differences.md
+++ b/differences.md
@@ -68,7 +68,7 @@
| JETSTREAM (internal) | Y | N | |
| ACCOUNT (internal) | Y | Y | Lazy per-account InternalClient with import/export subscription support |
| WebSocket clients | Y | Y | Custom frame parser, permessage-deflate compression, origin checking, cookie auth |
-| MQTT clients | Y | N | |
+| MQTT clients | Y | Partial | JWT connection-type constants + config parsing; no MQTT transport yet |
### Client Features
| Feature | Go | .NET | Notes |
@@ -204,7 +204,7 @@ Go implements a sophisticated slow consumer detection system:
| Username/password | Y | Y | |
| Token | Y | Y | |
| NKeys (Ed25519) | Y | Y | .NET has framework but integration is basic |
-| JWT validation | Y | Y | `NatsJwt` decode/verify, `JwtAuthenticator` with account resolution + revocation |
+| JWT validation | Y | Y | `NatsJwt` decode/verify, `JwtAuthenticator` with account resolution + revocation + `allowed_connection_types` enforcement |
| Bcrypt password hashing | Y | Y | .NET supports bcrypt (`$2*` prefix) with constant-time fallback |
| TLS certificate mapping | Y | Y | X500DistinguishedName with full DN match and CN fallback |
| Custom auth interface | Y | N | |
@@ -268,7 +268,7 @@ Go implements a sophisticated slow consumer detection system:
- ~~Tags/metadata~~ — `Tags` dictionary implemented in `NatsOptions`
- ~~OCSP configuration~~ — `OcspConfig` with 4 modes (Auto/Always/Must/Never), peer verification, and stapling
- ~~WebSocket options~~ — `WebSocketOptions` with port, compression, origin checking, cookie auth, custom headers
-- MQTT options
+- ~~MQTT options~~ — `mqtt {}` config block parsed with all Go `MQTTOpts` fields; no listener yet
- ~~Operator mode / account resolver~~ — `JwtAuthenticator` + `IAccountResolver` + `MemAccountResolver` with trusted keys
---
@@ -317,7 +317,7 @@ Go implements a sophisticated slow consumer detection system:
| Subscription detail mode | Y | N | |
| TLS peer certificate info | Y | N | |
| JWT/IssuerKey/Tags fields | Y | N | |
-| MQTT client ID filtering | Y | N | |
+| MQTT client ID filtering | Y | Y | `mqtt_client` query param filters open and closed connections |
| Proxy info | Y | N | |
---
diff --git a/src/NATS.Server/Auth/IAuthenticator.cs b/src/NATS.Server/Auth/IAuthenticator.cs
index 3783c88..abb8db3 100644
--- a/src/NATS.Server/Auth/IAuthenticator.cs
+++ b/src/NATS.Server/Auth/IAuthenticator.cs
@@ -1,4 +1,5 @@
using System.Security.Cryptography.X509Certificates;
+using NATS.Server.Auth.Jwt;
using NATS.Server.Protocol;
namespace NATS.Server.Auth;
@@ -13,4 +14,11 @@ public sealed class ClientAuthContext
public required ClientOptions Opts { get; init; }
public required byte[] Nonce { get; init; }
public X509Certificate2? ClientCertificate { get; init; }
+
+ ///
+ /// The type of connection (e.g., "STANDARD", "WEBSOCKET", "MQTT", "LEAFNODE").
+ /// Used by JWT authenticator to enforce allowed_connection_types claims.
+ /// Defaults to "STANDARD" for regular NATS client connections.
+ ///
+ public string ConnectionType { get; init; } = JwtConnectionTypes.Standard;
}
diff --git a/src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs b/src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs
new file mode 100644
index 0000000..59d2418
--- /dev/null
+++ b/src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs
@@ -0,0 +1,34 @@
+namespace NATS.Server.Auth.Jwt;
+
+internal static class JwtConnectionTypes
+{
+ public const string Standard = "STANDARD";
+ public const string Websocket = "WEBSOCKET";
+ public const string Leafnode = "LEAFNODE";
+ public const string LeafnodeWs = "LEAFNODE_WS";
+ public const string Mqtt = "MQTT";
+ public const string MqttWs = "MQTT_WS";
+ public const string InProcess = "INPROCESS";
+
+ private static readonly HashSet Known =
+ [
+ Standard, Websocket, Leafnode, LeafnodeWs, Mqtt, MqttWs, InProcess,
+ ];
+
+ public static (HashSet Valid, bool HasUnknown) Convert(IEnumerable? values)
+ {
+ var valid = new HashSet(StringComparer.Ordinal);
+ var hasUnknown = false;
+ if (values is null) return (valid, false);
+
+ foreach (var raw in values)
+ {
+ var up = (raw ?? string.Empty).Trim().ToUpperInvariant();
+ if (up.Length == 0) continue;
+ if (Known.Contains(up)) valid.Add(up);
+ else hasUnknown = true;
+ }
+
+ return (valid, hasUnknown);
+ }
+}
diff --git a/src/NATS.Server/Auth/JwtAuthenticator.cs b/src/NATS.Server/Auth/JwtAuthenticator.cs
index f28a155..126fb83 100644
--- a/src/NATS.Server/Auth/JwtAuthenticator.cs
+++ b/src/NATS.Server/Auth/JwtAuthenticator.cs
@@ -95,6 +95,24 @@ public sealed class JwtAuthenticator : IAuthenticator
}
}
+ // 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;
diff --git a/src/NATS.Server/Configuration/ConfigProcessor.cs b/src/NATS.Server/Configuration/ConfigProcessor.cs
index 88b36ae..ae593b1 100644
--- a/src/NATS.Server/Configuration/ConfigProcessor.cs
+++ b/src/NATS.Server/Configuration/ConfigProcessor.cs
@@ -245,6 +245,12 @@ public static class ConfigProcessor
opts.ReconnectErrorReports = ToInt(value);
break;
+ // MQTT
+ case "mqtt":
+ if (value is Dictionary mqttDict)
+ ParseMqtt(mqttDict, opts, errors);
+ break;
+
// Unknown keys silently ignored (cluster, jetstream, gateway, leafnode, etc.)
default:
break;
@@ -620,6 +626,145 @@ public static class ConfigProcessor
opts.Tags = tags;
}
+ // ─── MQTT parsing ────────────────────────────────────────────────
+ // Reference: Go server/opts.go parseMQTT (lines ~5443-5541)
+
+ private static void ParseMqtt(Dictionary dict, NatsOptions opts, List errors)
+ {
+ var mqtt = opts.Mqtt ?? new MqttOptions();
+
+ foreach (var (key, value) in dict)
+ {
+ switch (key.ToLowerInvariant())
+ {
+ case "listen":
+ var (host, port) = ParseHostPort(value);
+ if (host is not null) mqtt.Host = host;
+ if (port is not null) mqtt.Port = port.Value;
+ break;
+ case "port":
+ mqtt.Port = ToInt(value);
+ break;
+ case "host" or "net":
+ mqtt.Host = ToString(value);
+ break;
+ case "no_auth_user":
+ mqtt.NoAuthUser = ToString(value);
+ break;
+ case "tls":
+ if (value is Dictionary tlsDict)
+ ParseMqttTls(tlsDict, mqtt, errors);
+ break;
+ case "authorization" or "authentication":
+ if (value is Dictionary authDict)
+ ParseMqttAuth(authDict, mqtt, errors);
+ break;
+ case "ack_wait" or "ackwait":
+ mqtt.AckWait = ParseDuration(value);
+ break;
+ case "js_api_timeout" or "api_timeout":
+ mqtt.JsApiTimeout = ParseDuration(value);
+ break;
+ case "max_ack_pending" or "max_pending" or "max_inflight":
+ var pending = ToInt(value);
+ if (pending < 0 || pending > 0xFFFF)
+ errors.Add($"mqtt max_ack_pending invalid value {pending}, should be in [0..{0xFFFF}] range");
+ else
+ mqtt.MaxAckPending = (ushort)pending;
+ break;
+ case "js_domain":
+ mqtt.JsDomain = ToString(value);
+ break;
+ case "stream_replicas":
+ mqtt.StreamReplicas = ToInt(value);
+ break;
+ case "consumer_replicas":
+ mqtt.ConsumerReplicas = ToInt(value);
+ break;
+ case "consumer_memory_storage":
+ mqtt.ConsumerMemoryStorage = ToBool(value);
+ break;
+ case "consumer_inactive_threshold" or "consumer_auto_cleanup":
+ mqtt.ConsumerInactiveThreshold = ParseDuration(value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ opts.Mqtt = mqtt;
+ }
+
+ private static void ParseMqttAuth(Dictionary dict, MqttOptions mqtt, List errors)
+ {
+ foreach (var (key, value) in dict)
+ {
+ switch (key.ToLowerInvariant())
+ {
+ case "user" or "username":
+ mqtt.Username = ToString(value);
+ break;
+ case "pass" or "password":
+ mqtt.Password = ToString(value);
+ break;
+ case "token":
+ mqtt.Token = ToString(value);
+ break;
+ case "timeout":
+ mqtt.AuthTimeout = ToDouble(value);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private static void ParseMqttTls(Dictionary dict, MqttOptions mqtt, List errors)
+ {
+ foreach (var (key, value) in dict)
+ {
+ switch (key.ToLowerInvariant())
+ {
+ case "cert_file":
+ mqtt.TlsCert = ToString(value);
+ break;
+ case "key_file":
+ mqtt.TlsKey = ToString(value);
+ break;
+ case "ca_file":
+ mqtt.TlsCaCert = ToString(value);
+ break;
+ case "verify":
+ mqtt.TlsVerify = ToBool(value);
+ break;
+ case "verify_and_map":
+ var map = ToBool(value);
+ mqtt.TlsMap = map;
+ if (map) mqtt.TlsVerify = true;
+ break;
+ case "timeout":
+ mqtt.TlsTimeout = ToDouble(value);
+ break;
+ case "pinned_certs":
+ if (value is List