Using jwt as mosquitto auth backend

Hi, I was trying the Paho javascript client to listen to some MQTT messages I previously decoded at lora-app-server and show them in real time at the frontend. Auth works fine with username/password combo using mosquitto-auth-plug, but it’s too limited as it needs to connect to the client on login in order to send credentials (as we don’t want to store those on the localStorage), which breaks with a simple reload, forcing to login again.

So I was about to switch to the JWT backend at the auth plugin, but I’m not really sure how (or if possible) to respond with HTTP status codes when defining the gRPC API. Of course I could just set it as REST only API with the three required URIs, but it seems a little ugly considering everything else is defined as a gRPC service using the grpc-gateway.

So, has anyone done this before or know how to respond with different HTTP statuses?

Well, I didn’t find a way to respond with different status codes other than 200 and 500 (and 500 seems ugly for the case), and didn’t want to generate a separate API just for authenticating MQTT either, so I went the other way and forked the mosquitto auth plugin to add a backend that could deal with JWT tokens and json responses (and some missing TLS options in the plugin as well) from the regular lora-app-server API for the authentication.

It isn’t quite ready, I had some issues with a C json parser and am missing a couple of lines to finish the ACL checks in the API, but it’ll probably be tested and ready by monday. I’ll comment here when it’s done and share it for anyone that may find it useful.

In a similar note, I was thinking abut writing a Go plugin that does mosquitto authentication using cgo. I really like the idea of keeping everything in Go and stop depending on a C library (it’d be some work to keep the fork up to date and having everything in a common language could benefit from loraserver community contributions), but I’m not really sure how to go about it, so if anyone has experience using cgo or ideas for it, it’d be great!

I’ve been a bit occupied with other issues lately, but finally got some time today to wrap this up and now it’s finished:

The fork adds a new backend, written in Go using cgo, to use JWT tokens with a json API (such as the lora-app-server rest api) for authentication. Paired with something like this at the lora-app-server side (just showing some custom internal/api bits to give an example), it works great for mosquitto auth:

// Auth checks that the jwt is correct and the user is active.
func (a *MQTTAuthAPI) Auth(ctx context.Context, req *pb.GetUserAuthRequest) (*pb.AuthResponse, error) {

	fmt.Printf("Auth req: %v\n", req)
	if err := a.validator.Validate(ctx,
		auth.ValidateActiveUser()); err != nil {
		fmt.Printf("auth strange error: %v\n", err)
		return &pb.AuthResponse{Ok: false, Error: "unauthorized user"}, nil
	}

	username, err := a.validator.GetUsername(ctx)
	if nil != err {
		return &pb.AuthResponse{Ok: false, Error: "couldn't get username"}, nil
	}

	log.Printf("auth passed for user: %s", username)

	return &pb.AuthResponse{Ok: true, Error: "none"}, nil

}

// Superuser checks that the user is gneral admin.
func (a *MQTTAuthAPI) Superuser(ctx context.Context, req *pb.GetSuperuserAuthRequest) (*pb.AuthResponse, error) {

	fmt.Printf("Superuser req: %v\n", req)
	if err := a.validator.Validate(ctx,
		auth.ValidateActiveUser()); err != nil {
		fmt.Printf("superuser strange error: %v\n", err)
		return &pb.AuthResponse{Ok: false, Error: "unauthorized user"}, nil
	}

	isAdmin, err := a.validator.GetIsAdmin(ctx)

	if err != nil || !isAdmin {
		fmt.Printf("Not admin user\n")
		return &pb.AuthResponse{Ok: false, Error: "not admin user"}, nil
	}

	username, err := a.validator.GetUsername(ctx)
	if nil != err {
		fmt.Printf("Couldn't get username\n")
		return &pb.AuthResponse{Ok: false, Error: "couldn't get username"}, nil
	}

	log.Printf("superuser passed for user: %s with isAdmin", username, isAdmin)

	return &pb.AuthResponse{Ok: true, Error: "none"}, nil

}

// AclCheck checks that the user can read from the given topic.
func (a *MQTTAuthAPI) AclCheck(ctx context.Context, req *pb.GetAclCheckAuthRequest) (*pb.AuthResponse, error) {

	topicArray := strings.Split(req.Topic, "/")

	if topicArray[len(topicArray)-1] != "data" || topicArray[0] != "application" || topicArray[2] != "device" {
		fmt.Printf("ACL wrong format: %v\n", req)
		return &pb.AuthResponse{Ok: false, Error: "acl wrong format"}, nil
	}

	if req.Acc != 1 {
		fmt.Printf("ACL strange request: %v\n", req)
		return &pb.AuthResponse{Ok: false, Error: "acl wrong acc"}, nil
	}

	var eui lorawan.EUI64
	if err := eui.UnmarshalText([]byte(topicArray[3])); err != nil {
		return &pb.AuthResponse{Ok: false, Error: "wrong devEUI"}, nil
	}

	if err := a.validator.Validate(ctx,
		auth.ValidateDeviceAccess(eui, auth.Read)); err != nil {
		fmt.Printf("acl unauthorized: %v\n", err)
		return &pb.AuthResponse{Ok: false, Error: "unauthorized user"}, nil
	}

	return &pb.AuthResponse{Ok: true, Error: "none"}, nil

}

Of course lora-app-server needs the service and messages defined in an api proto and to register that service, but this shows the general idea.

It’s also important to notice that it keeps true to the original plugin, so the config files and building process remain the same, just make and then link to it at the mosquitto conf.

I’m currently working on a full Go mosquitto auth plugin (I like Go :sweat_smile:) that just uses cgo as a middle man to implement the auth plugin necessary functions, and does all the logic and handles different backends (to start, I was thinking Redis, Postgres, Files and JWT json) in pure Go, but this simple addition is good enough for simple requirements such as JWT mosquitto authentication. If anyone is interested, I can keep the fork up to date with the original plugin so it’s useful in the long run.