feat(config, docs): add JWT configuration options for MQTT authentication

- Introduced `jwt_ttl_seconds` and `jwt_renewal_interval` settings in `config.ini.example` for global JWT management, allowing for better control over token expiration and renewal intervals.
- Updated documentation in `packet-capture.md` to clarify the usage of global and per-broker JWT settings, enhancing user understanding of authentication configurations.
- Refactored `PacketCaptureService` to incorporate new JWT settings, ensuring consistent handling of token lifetimes and renewal processes.
This commit is contained in:
agessaman
2026-05-15 20:46:05 -07:00
parent c91baf1fdf
commit 9d24e4824c
4 changed files with 644 additions and 424 deletions
+9 -1
View File
@@ -1605,6 +1605,9 @@ mqtt1_topic_packets = meshcore/{IATA}/{PUBLIC_KEY}/packets
mqtt1_websocket_path = /mqtt
mqtt1_client_id =
mqtt1_upload_packet_types =
# Optional per-broker JWT (inherit globals if omitted):
# mqtt1_jwt_ttl_seconds = 3600 # JWT exp claim: iat + this many seconds
# mqtt1_jwt_renewal_interval = 1800 # Refresh password this often; use < ttl; 0 = no renewal loop
# MQTT Broker 2 - Let's Mesh Analyzer (EU)
mqtt2_enabled = true
@@ -1627,7 +1630,12 @@ stats_in_status_enabled = true
# Stats refresh interval (seconds)
stats_refresh_interval = 300
# JWT renewal interval (seconds, 0 = disabled)
# JWT TTL in seconds: exp iat in the MQTT auth token (default 86400 = 24 hours)
jwt_ttl_seconds = 86400
# JWT renewal interval (seconds): default cadence for proactive token refresh per broker.
# 0 here makes the default broker renewal interval 0 (no renewal task unless a broker sets mqttN_jwt_renewal_interval > 0).
# Tokens are still created at MQTT connect and on reconnect.
jwt_renewal_interval = 86400
# Health check interval (seconds, 0 = disabled)
+15 -2
View File
@@ -129,12 +129,25 @@ Placeholders:
- `{PUBLIC_KEY}` - Device public key (uppercase)
- `{public_key}` - Device public key (lowercase)
### Status Publishing
### Status Publishing and MQTT auth (JWT)
Two separate settings:
- **`jwt_ttl_seconds`** (global) / **`mqttN_jwt_ttl_seconds`** (per broker): lifetime of the JWT in the `exp` claim (`exp = iat + ttl`). Use this when the broker enforces a maximum token lifetime (e.g. 60 minutes → `3600`).
- **`jwt_renewal_interval`** (global) / **`mqttN_jwt_renewal_interval`** (per broker): how often the bot refreshes the MQTT password for that broker. Set **less than** the TTL (e.g. TTL 3600s and renewal every 1800s) so the connection does not outlive the token.
Per-broker keys override the global values for that broker only. Omit them to inherit globals.
```ini
stats_in_status_enabled = true # Include device stats in status
stats_refresh_interval = 300 # Publish status every 5 minutes
jwt_renewal_interval = 86400 # Renew JWT every 24 hours
jwt_ttl_seconds = 86400 # Default JWT exp iat (24 hours) for all brokers unless overridden
jwt_renewal_interval = 43200 # Default proactive refresh cadence (12 hours); 0 = no renewal task
# Example on a broker that requires 60-minute tokens and refresh halfway through:
# mqtt1_jwt_ttl_seconds = 3600
# mqtt1_jwt_renewal_interval = 1800
```
---
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
"""PacketCapture MQTT broker JWT renewal interval and TTL parsing."""
from __future__ import annotations
import configparser
from unittest.mock import MagicMock
import pytest
from modules.service_plugins.packet_capture_service import PacketCaptureService
def _bot_from_ini(ini: str) -> MagicMock:
cp = configparser.ConfigParser()
cp.read_string(ini.strip())
bot = MagicMock()
bot.config = cp
return bot
def test_parse_mqtt_brokers_defaults_renewal_and_ttl():
bot = _bot_from_ini(
"""
[PacketCapture]
enabled = false
mqtt1_server = broker.example
"""
)
svc = object.__new__(PacketCaptureService)
svc.bot = bot
brokers = PacketCaptureService._parse_mqtt_brokers(svc, bot.config)
assert len(brokers) == 1
assert brokers[0]["jwt_renewal_interval"] == 43200
assert brokers[0]["jwt_ttl_seconds"] == 86400
def test_parse_mqtt_brokers_global_overrides():
bot = _bot_from_ini(
"""
[PacketCapture]
enabled = false
jwt_renewal_interval = 7200
jwt_ttl_seconds = 3600
mqtt1_server = a.example
mqtt2_server = b.example
"""
)
svc = object.__new__(PacketCaptureService)
svc.bot = bot
brokers = PacketCaptureService._parse_mqtt_brokers(svc, bot.config)
assert len(brokers) == 2
for b in brokers:
assert b["jwt_renewal_interval"] == 7200
assert b["jwt_ttl_seconds"] == 3600
def test_parse_mqtt_brokers_per_broker_overrides():
bot = _bot_from_ini(
"""
[PacketCapture]
enabled = false
jwt_renewal_interval = 7200
jwt_ttl_seconds = 86400
mqtt1_server = short-ttl.example
mqtt1_jwt_ttl_seconds = 3600
mqtt1_jwt_renewal_interval = 1800
mqtt2_server = inherit.example
"""
)
svc = object.__new__(PacketCaptureService)
svc.bot = bot
brokers = PacketCaptureService._parse_mqtt_brokers(svc, bot.config)
assert len(brokers) == 2
assert brokers[0]["host"] == "short-ttl.example"
assert brokers[0]["jwt_ttl_seconds"] == 3600
assert brokers[0]["jwt_renewal_interval"] == 1800
assert brokers[1]["jwt_ttl_seconds"] == 86400
assert brokers[1]["jwt_renewal_interval"] == 7200
def test_parse_mqtt_brokers_renewal_zero_override():
bot = _bot_from_ini(
"""
[PacketCapture]
enabled = false
jwt_renewal_interval = 3600
mqtt1_server = no-renew.example
mqtt1_jwt_renewal_interval = 0
"""
)
svc = object.__new__(PacketCaptureService)
svc.bot = bot
brokers = PacketCaptureService._parse_mqtt_brokers(svc, bot.config)
assert brokers[0]["jwt_renewal_interval"] == 0
def test_auth_token_iat_exp_clamps_non_positive_ttl():
svc = object.__new__(PacketCaptureService)
svc.jwt_ttl_seconds = 7200
iat, exp = PacketCaptureService._auth_token_iat_exp(svc, {"jwt_ttl_seconds": 0})
assert exp - iat == 86400
assert exp == iat + 86400
iat2, exp2 = PacketCaptureService._auth_token_iat_exp(svc, {"jwt_ttl_seconds": -10})
assert exp2 - iat2 == 86400
def test_auth_token_iat_exp_uses_broker_then_global():
svc = object.__new__(PacketCaptureService)
svc.jwt_ttl_seconds = 999
iat, exp = PacketCaptureService._auth_token_iat_exp(svc, {"jwt_ttl_seconds": 120})
assert exp - iat == 120
@pytest.mark.parametrize(
"ttl,phrase",
[
(3600, "1 hour"),
(7200, "2 hours"),
(60, "1 minute"),
(180, "3 minutes"),
(90, "90s"),
],
)
def test_jwt_ttl_log_phrase(ttl: int, phrase: str):
assert PacketCaptureService._jwt_ttl_log_phrase(ttl) == phrase