HiveMQ

Authentication, Authorization and ClientInitializer

This topic is a continuation of Store massages in MsSQL, but this time about security.

I’m building a simple extension that will read the username and password from the DB.
I’ve based my code on https://github.com/hivemq/hivemq-file-rbac-extension

I have some questions about authentication and authorization.

Take a look at https://github.com/hivemq/hivemq-file-rbac-extension/blob/96009fc4a3353857dea8ee1881b06ba265e0481c/src/main/java/com/hivemq/extensions/rbac/FileAuthAuthenticator.java#L97-L101

Here topic permissions are set when doing authentication.
Does it mean that I can skip authorization and do everything during authentication?

What is the best practice?

Should I add permissions to the authenticator? Like shown here https://github.com/hivemq/hivemq-file-rbac-extension/blob/96009fc4a3353857dea8ee1881b06ba265e0481c/src/main/java/com/hivemq/extensions/rbac/FileAuthAuthenticator.java#L98?

When can I use ClientInitializer for?

My use case is as follows:

  • I have users and devices.
  • Each user can own any number of devices
  • The device can only publish to their channels (channel d-{device-id}/temp)
  • The user can subscribe to any or all channels that the devices he owns publish. SO if he has two devices he can subscribe to d-1/temp and d-2/temp, but also to +/temp, this will be used for WebSocket connection (to see live data changes)

My first idea was to call the database in the authenticator and store devices id’s in ConnectionAttributeStore and validate topics in authorizer, but if I can do everything during authentication then I will be able to avoid creating authorizer.

Also, I’m trying to implement both SubscriptionAuthorizer and PublishAuthorizer, but IntelliJ shows me an error for getTopicFilter function. My code is based on https://www.hivemq.com/docs/hivemq/4.4/extensions/authorization.html#_example_usage_3

Is the sample in the docs incomplete?
EDIT:
the sample in the docs is wrong.
It should be:
if (input.getSubscription().getTopicFilter().startsWith("admin")) {
instead of
if (input.getTopicFilter().startsWith("admin")) {

While trying to implement both SubscriptionAuthorizer and PublishAuthorizer I can’t get authorizeSubscribe to work.
authorizePublish works fine, but the second isn’t.
@FloLi any ideas why? I’m using Community Edition (if that changes something).

package com.test;

import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.auth.PublishAuthorizer;
import com.hivemq.extension.sdk.api.auth.SubscriptionAuthorizer;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerOutput;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerOutput;
import com.hivemq.extension.sdk.api.client.parameter.ConnectionAttributeStore;
import com.hivemq.extension.sdk.api.packets.disconnect.DisconnectReasonCode;
import com.hivemq.extension.sdk.api.packets.general.UserProperties;
import com.hivemq.extension.sdk.api.packets.publish.PublishPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;

public class DatabaseAuthorizer implements SubscriptionAuthorizer, PublishAuthorizer {

    private static final @NotNull
    Logger log = LoggerFactory.getLogger(DatabaseAuthorizer.class);

    @Override
    public void authorizeSubscribe(@NotNull SubscriptionAuthorizerInput input, @NotNull SubscriptionAuthorizerOutput output) {

        log.info("authorizeSubscribe");

        final ConnectionAttributeStore attributeStore = input.getConnectionInformation().getConnectionAttributeStore();
        Optional<String> deviceId  = attributeStore.getAsString("deviceId");

        if(!deviceId.isPresent()){
            log.info("No deviceId in ConnectionAttributeStore");
        } else
        {
            log.info("DeviceId in ConnectionAttributeStore = '{}'", deviceId.get());
        }

        //allow every Topic Filter starting with "admin"
        if (input.getSubscription().getTopicFilter().startsWith("admin")) {
            output.authorizeSuccessfully();
            return;
        }

        //disallow a shared subscription
        if (input.getSubscription().getTopicFilter().startsWith("$shared")) {
            output.failAuthorization();
            return;
        }

        //get the user properties from the SUBSCRIBE packet
        final UserProperties userProperties = input.getUserProperties();

        //disconnect all clients with a user property "notallowed" set.
        if (userProperties.getFirst("notallowed").isPresent()) {
            //Use a custom reason code and reason string for the server sent DISCONNECT
            output.disconnectClient(DisconnectReasonCode.ADMINISTRATIVE_ACTION, "User property not allowed");
            return;
        }

        //otherwise let the other extension or default permissions decide
        output.nextExtensionOrDefault();
    }

    @Override
    public void authorizePublish(@NotNull PublishAuthorizerInput input, @NotNull PublishAuthorizerOutput output) {

        log.info("authorizePublish");

        //get the PUBLISH packet contents
        final PublishPacket publishPacket = input.getPublishPacket();

        //allow every Topic Filter starting with "admin"
        if (publishPacket.getTopic().startsWith("admin")) {
            output.authorizeSuccessfully();
            return;
        }

        //disallow publishes to forbidden topics
        if (publishPacket.getTopic().startsWith("forbidden")) {
            output.failAuthorization();
            return;
        }

        //get the user properties from the PUBLISH packet
        final UserProperties userProperties = publishPacket.getUserProperties();

        //disconnect all clients with a user property "notallowed" set.
        if (userProperties.getFirst("notallowed").isPresent()) {
            //Use a custom reason code and reason string for the server sent DISCONNECT
            output.disconnectClient(DisconnectReasonCode.ADMINISTRATIVE_ACTION, "User property not allowed");
            return;
        }

        //otherwise let the other extension or default permissions decide
        output.nextExtensionOrDefault();
    }
}

When I try to publish I see in log authorizePublish but when I try to subscribe I don’t see authorizeSubscribe.

I register my class like this:
securityRegistry.setAuthorizerProvider(authorizerProviderInput -> new DatabaseAuthorizer());

Hi misiu,

Thanks for the advice for the documentation issue. :slight_smile:

Yes, thats correct.

Depends on your usecase.

Simple authentication (username,password) will be called once and for updates you need to reconnect, the CONNECT performance will decrease but on the other hand the PUBLISH / SUBSCRIBE performance would increase if everything will be done in authentication.

Let’s say if your clients do not need live updates for the permissions and they publish/subscribe very often compared to connect/disconnect than authentication would be a good way.

You can use a ClientInitializer to add any kind of Interceptor like: https://www.hivemq.com/docs/hivemq/4.4/extensions/interceptors.html#publish-inbound-interceptor

the code looks totally fine. This should work.
Do you have any other extension activated that has a SubscriptionAuthorizer?
This could cause that the other one will never be called.

Kind regards,
Flo

Thank you for reply.
I’ve created a new extension and copy/paste code from my old one. Now everything works fine :slight_smile:
For now I’m only adding log messages in authorizePublish and authorizeSubscribe
Now I need to do the logic.

I have one more question, this time more Java-related (as I wrote before I’m not a Java programmer)
When doing authentication I’m checking for login/pass pair in the database. Then I’m querying for all the device_id’s that the user owns.
I’m using them to build topic permissions (similar to file rbac extension).

I’d like to store id’s into ConnectionAttributeStore to be able to access the same list in my authorization class.

Here is my code that I’ve added to onConnect

final ConnectionAttributeStore attributeStore = input.getConnectionInformation().getConnectionAttributeStore();
var list = new ArrayList<String>();
list.add("d-1");
list.add("d-2");
list.add("d3");
attributeStore.put("devices",??);

How should I serialize my list to ByteBuffer?

For now, I’m serializing/deserializing with above code, but I’m sure there is a better way to store and read a list of strings in ConnectionAttributeStore

var list = new ArrayList<String>();
list.add("d-1");
list.add("d-2");
list.add("d3");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(list);
byte[] bytes = bos.toByteArray();
ByteBuffer buffer = ByteBuffer.wrap(bytes);

ByteArrayInputStream bis = new ByteArrayInputStream(buffer.array());
ObjectInput oi = new ObjectInputStream(bis);
ArrayList<String> devices = (ArrayList<String>) oi.readObject();

@FloLi additional question about authentication and topic permissions (continuation of Store massages in MsSQL)

currently in onConnect I’m using below code to set permissions per user:

final ArrayList<TopicPermission> topicPermissions = new ArrayList<>();
for (String deviceId: user.get().getDevices()) {
    final TopicPermission publishPermission = Builders.topicPermission()
            .topicFilter(deviceId+"/settings")
            .activity(TopicPermission.MqttActivity.PUBLISH)
            .type(TopicPermission.PermissionType.ALLOW)
            .build();
    topicPermissions.add(publishPermission);

    final TopicPermission subscribePermission = Builders.topicPermission()
            .topicFilter(deviceId+"/values")
            .activity(TopicPermission.MqttActivity.SUBSCRIBE)
            .type(TopicPermission.PermissionType.ALLOW)
            .build();
    topicPermissions.add(subscribePermission);
}

output.getDefaultPermissions().addAll(topicPermissions);
output.getDefaultPermissions().setDefaultBehaviour(DefaultAuthorizationBehaviour.DENY);

output.authenticateSuccessfully();

this works great in basic scenario, but I want to add support for subscribing to +/values topic.

Idea is that when user1 subscribes to that topic he will only get messages from devices 1,2 and 3 (because he owns them).

In the linked response you showed that this can be done in authorizeSubscribe, but can I set topic permissions in onConnect? This way user could subscribe to get values from all devices and the broker would handle sending values from device to correct user.

This would be an awesome feature to have!