客户端无法通过 JWT 方式接入 EMQX, hmac, from password

环境信息

  • EMQX 版本:5.0.1
  • 操作系统及版本:Debian 11
  • 其他

问题描述

按照官网 https://www.emqx.io/docs/zh/v5.0/security/authn/jwt.html#配置与使用
为 EMQX Broker 配置了 JWT 权限控制

根据文档描述,满足如下条件,应该可以正确连接

  • 生成 JWT token 的 key 与 Broker 中配置的 key 一致 , 我这里都是默认的 emqxsecret
  • password 中填写 JWT token

但是接下来,无论通过

  • MQTTX (GUI)
  • Golang SDK ( paho.mqtt.golang)
    都无法成功链接到 Broker

配置文件及日志

Broker 配置

#cat /var/lib/emqx/configs/cluster-override.conf
authentication {
  algorithm = "hmac-based"
  from = "password"
  mechanism = "jwt"
  secret = "emqxsecret"
  "secret_base64_encoded" = false
  use_jwks = false
  verify_claims {}
』

MQTTX 配置

mqttx-jwt-connecting

Golang SDK 代码

package main

import (
	"fmt"
	"os"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	"github.com/spf13/pflag"
)

func main() {
	var broker string
	var topic string
	var clientID string
	var action string
	var msg string
	var passwd string

	pflag.StringVarP(&broker, "broker", "b", "x.x.x.x:1883", "Address of broker, ip:port")
	pflag.StringVarP(&topic, "topic", "t", "testtopic", "topic name")
	pflag.StringVar(&clientID, "clientid", "someclient", "client id for mqtt")
	pflag.StringVar(&action, "action", "", "action: pub or sub")
	pflag.StringVar(&msg, "msg", "", "msg to be published")
	pflag.StringVar(&passwd, "passwd", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTY1OTM1MzYyNX0.FTkF3Y8vjf1QUyrSdgdne6_7bu0g-oGHehh8_ZGxn4g", "password for mqtt")

	pflag.Parse()

	if action != "pub" && action != "sub" {
		fmt.Println("Invalid setting for --action, must be pub or sub")
		return
	}

	opts := mqtt.NewClientOptions()
	opts.AddBroker(broker)
	opts.SetClientID(clientID)
	if passwd != "" {
		opts.SetPassword(passwd)
	}

	fmt.Println(opts)
	if action == "pub" {
		// publish a message
		client := mqtt.NewClient(opts)
		if token := client.Connect(); token.Wait() && token.Error() != nil {
			panic(token.Error())
		}

		token := client.Publish(topic, 0, false, msg)
		token.Wait()

		//client.Disconnect(250)
		//fmt.Println("Publisher Disconnected")

	} else {
		// subscribe a topic
		choke := make(chan [2]string)

		opts.SetDefaultPublishHandler(func(client mqtt.Client, msg mqtt.Message) {
			choke <- [2]string{msg.Topic(), string(msg.Payload())}
		})

		client := mqtt.NewClient(opts)
		if token := client.Connect(); token.Wait() && token.Error() != nil {
			panic(token.Error())
		}

		if token := client.Subscribe(topic, 0, nil); token.Wait() && token.Error() != nil {
			fmt.Println(token.Error())
			os.Exit(1)
		}

		incoming := <-choke
		fmt.Printf("TOpic: %s. MESSAGE: %S\n", incoming[0], incoming[1])

		client.Disconnect(250)
		fmt.Println("Subscriber Disconnectes")

	}
}

运行报错

./mqtt --action pub --msg shaya
&{[tcp://x.x.x.x:1883] someclient  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTY1OTM1MzYyNX0.FTkF3Y8vjf1QUyrSdgdne6_7bu0g-oGHehh8_ZGxn4g <nil> true true false  [] 0 false 0 false <nil> 30 10s 30s 10m0s true 30s false <nil> <nil> <nil> 0x605be0 <nil> <nil> 0s 0 false map[] 0xc0000ae138 0 0xc0000b8300 <nil>}
panic: not Authorized

goroutine 1 [running]:
main.main()
        /home/foo/workspace/go/mqtt-demo/cmd/mqtt/main.go:45 +0x99d
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTY1OTM1MzYyNX0.FTkF3Y8vjf1QUyrSdgdne6_7bu0g-oGHehh8_ZGxn4g

这个 JWT 看起来是对的

方便开下1883的抓包 和 emqx 的 debug 日志在复现一次,我看下里面的结果?

@heeejianbo OK 。以下是日志和 tcpdump 结果,辛苦

更新的 jwt

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTY1OTQxODg2NH0.IdoThrnfagJ36GGX36aD7Fq8eR7ZMBze0VvVONtl7MQ

debug logging

2022-08-02T13:11:40.976081+08:00 [debug] line: 631, mfa: emqx_connection:terminate/2, msg: emqx_connection_terminated, packet: [], payload: [], peername: 172.16.0.160:49484, reason: {shutdown,tcp_closed}, tag: SOCKET
2022-08-02T13:11:40.976479+08:00 [info] line: 636, mfa: emqx_connection:terminate/2, msg: terminate, peername: 172.16.0.160:49484, reason: {shutdown,tcp_closed}
2022-08-02T13:11:50.254383+08:00 [debug] line: 150, mfa: emqx_retainer_mnesia:store_retained/2, msg: message_retained, topic: $SYS/brokers/emqx@127.0.0.1/version
2022-08-02T13:11:50.255192+08:00 [debug] line: 150, mfa: emqx_retainer_mnesia:store_retained/2, msg: message_retained, topic: $SYS/brokers/emqx@127.0.0.1/sysdescr
2022-08-02T13:11:50.255796+08:00 [debug] line: 150, mfa: emqx_retainer_mnesia:store_retained/2, msg: message_retained, topic: $SYS/brokers

tcpdump 结果

tcpdump -i eth0 -nn -v port 1883

tcpdump -i eth0 -nn -v port 1883
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
13:11:30.991527 IP (tos 0x0, ttl 62, id 17828, offset 0, flags [DF], proto TCP (6), length 60)
    172.16.0.160.49484 > 10.2.0.3.1883: Flags [S], cksum 0xe27e (correct), seq 2365768400, win 64240, options [mss 1360,sackOK,TS val 3951800376 ecr 0,nop,wscale 7], length 0
13:11:30.991563 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.2.0.3.1883 > 172.16.0.160.49484: Flags [S.], cksum 0xb6e3 (incorrect -> 0xc4c7), seq 82598339, ack 2365768401, win 1448, options [mss 1460,sackOK,TS val 2210869266 ecr 3951800376,nop,wscale 9], length 0
13:11:31.025817 IP (tos 0x0, ttl 62, id 17829, offset 0, flags [DF], proto TCP (6), length 52)
    172.16.0.160.49484 > 10.2.0.3.1883: Flags [.], cksum 0xf72a (correct), ack 1, win 502, options [nop,nop,TS val 3951800405 ecr 2210869266], length 0
13:11:40.975844 IP (tos 0x0, ttl 62, id 17830, offset 0, flags [DF], proto TCP (6), length 52)
    172.16.0.160.49484 > 10.2.0.3.1883: Flags [F.], cksum 0xd03e (correct), seq 1, ack 1, win 502, options [nop,nop,TS val 3951810368 ecr 2210869266], length 0
13:11:40.975977 IP (tos 0x0, ttl 64, id 3763, offset 0, flags [DF], proto TCP (6), length 52)
    10.2.0.3.1883 > 172.16.0.160.49484: Flags [F.], cksum 0xb6db (incorrect -> 0xab2f), seq 1, ack 2, win 3, options [nop,nop,TS val 2210879251 ecr 3951810368], length 0
13:11:40.991214 IP (tos 0x0, ttl 62, id 17831, offset 0, flags [DF], proto TCP (6), length 52)
    172.16.0.160.49484 > 10.2.0.3.1883: Flags [.], cksum 0xa929 (correct), ack 2, win 502, options [nop,nop,TS val 3951810387 ecr 2210879251], length 0

此时发生一件奇怪的事情。

EMQX 方面,仍然保持 JWT 配置,且 Auth 配置中只有 JWT,并无 password 认证
MQTTX 方面,在链接配置中,仍然保持 password 中写入 jwt, 但是同时在 username 中随便填了一个用户名,就可以成功 connect

此时的 日志

2022-08-02T19:25:27.986682+08:00 [debug] line: 792, mfa: emqx_connection:handle_incoming/2, msg: mqtt_packet_received, packet: CONNECT(Q0, R0, D0),ClientId=mqttx_95198653, ProtoName=MQTT, ProtoVsn=5, CleanStart=true, KeepAlive=60, Username=nobody, Password=******, payload: [], peername: 172.16.0.160:35828, tag: MQTT
2022-08-02T19:25:27.987014+08:00 [debug] clientid: mqttx_95198653, line: 350, mfa: emqx_channel:handle_in/2, msg: mqtt_packet_received, packet: CONNECT(Q0, R0, D0),ClientId=mqttx_95198653, ProtoName=MQTT, ProtoVsn=5, CleanStart=true, KeepAlive=60, Username=nobody, Password=******, payload: [], peername: 172.16.0.160:35828, tag: MQTT
2022-08-02T19:25:27.988408+08:00 [debug] client_id: <<"mqttx_95198653">>, clientid: mqttx_95198653, line: 155, mfa: emqx_cm:insert_channel_info/3, msg: insert_channel_info, peername: 172.16.0.160:35828
2022-08-02T19:25:27.988599+08:00 [debug] clientid: mqttx_95198653, line: 838, mfa: emqx_connection:serialize_and_inc_stats_fun/1, msg: mqtt_packet_sent, packet: CONNACK(Q0, R0, D0),AckFlags=0, ReasonCode=0, payload: [], peername: 172.16.0.160:35828, tag: MQTT
2022-08-02T19:25:39.831336+08:00 [debug] line: 175, mfa: emqx_authn_jwks_client:refresh_jwks/1, msg: jwks_endpoint_request_ok, request_id: #Ref<0.97194578.2849767426.221621>
2022-08-02T19:25:39.831615+08:00 [debug] line: 83, mfa: emqx_authn_jwks_client:handle_info/2, msg: refresh_jwks_by_timer
2022-08-02T19:25:39.832191+08:00 [debug] line: 175, mfa: emqx_authn_jwks_client:refresh_jwks/1, msg: jwks_endpoint_request_ok, request_id: #Ref<0.97194578.2849767426.221636>
2022-08-02T19:25:39.832314+08:00 [debug] line: 175, mfa: emqx_authn_jwks_client:refresh_jwks/1, msg: jwks_endpoint_request_ok, request_id: #Ref<0.97194578.2849767426.221642>

而且这个用户名都是 xjb 写的,根本就不存在

你的这个token过期了…所以登录失败了,你 token 里面的 exp 转出来是这个:

2022-0802 05:41:04

试试这个

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTc1OTQxODg2NH0.vggyjrysA9V72yNqOo_FNCX7z4KWkqXOULxlZzod8Ao

注意下 exp 这个字段, emqx auth-jwt 是会校验这个字段的

@heeejianbo
应该可以确认不是你说的问题,大概有如下理由

exp 问题

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImZvbyIsImV4cCI6MTY1OTQxODg2NH0.IdoThrnfagJ36GGX36aD7Fq8eR7ZMBze0VvVONtl7MQ

这个 JWT 是我在 8 小时前发的帖子中的,也就是大概是今天中午13:00 之前。
JWT 解出来的 exp 是 1659418864, 换成东 8 区时间就是今天中午 13:41 ,也就是你回复中的 05:41 , 在当时是完全没有过期的

username/password 的问题

请看一下我后来的回复,只是没有重新编辑帖子,我当然是知道要更新 JWT 防止过期的。
在最新的回复中,我大约于今天下午 19:25 重新进行了 JWT 刷新/登陆等一系列操作
并且意外地发现,在 username 中随便胡乱写点东西,哪怕是空格符,就能成功 connet,而此时我的 emqx 只设置了 jwt 作为唯一的 auth 方式

我觉得 username 这个很可能是新版本中一个潜在的 bug


今天上午,我将 emqx 版本降低到 4.4.6 重新进行验证,测试 JWT 认证
基本上得到了一样的结果,即

  • 仅使用 JWT,无法完成连接
  • username 输入任意字符之后,可以完成连接

@heeejianbo 能否帮我确认一下,这是一个问题,还是 emqx 设计如此?

抱歉回复得晚了

我本地用 mqttx 测试过,确实连接不上。是由于 mqttx 不支持只填写 password,不填写 username 直接发起连接。这个应该是客户端实现的问题。

我猜 golang sdk 很有可能也是这个问题。

你可以通过抓包看看是否有发出 MQTT Connect 报文到 emqx。

但是我用 emqtt 是可以连接成功的

理解,辛苦了。

如果您确认是 client 问题

是否可以把 JWT 认证模式下,同时填写 username/password 作为一个可靠的 workaround 来使用 EMQX

如果官方觉得 ok ,我们就放心这么用了

这样没有问题的