Authorization Options for Your HiveMQ Extension

The HiveMQ Extension System offers multiple options for authorizing clients to publish and subscribe.

The simplest option are Default Permissions. These permissions can be set in an extension when a client connects or is initialized and are valid for the lifetime of an MQTT connection. The permissions represent a list of topics that a client is allowed or not allowed to publish/subscribe to.

Another option is using Publish Authorizer and Subscription Authorizer. These are extension callbacks, which are called by HiveMQ for every PUBLISH/SUBSCRIBE packet sent by a client. Authorizers provide a more flexible way to decide authorization based on the MQTT packet contents on a per packet basis.

Authorizers and Default Permissions can be combined for full flexibility.

For most use cases Default Permissions are the preferred way for authorization.

Default Permissions

Default Permissions represent authorized topics for a specific MQTT client that can be set by an extension and are then automatically applied by HiveMQ to every PUBLISH or SUBSCRIBE packet sent by this MQTT client. The default permissions for an MQTT client can be set in an extension when the client is authorized or initialized. Permissions are based on MQTT Topic Filters and specify if a client is allowed to PUBLISH/SUBSCRIBE to a specific topic or topic pattern.

Default permissions are tracked in an object of the class ModifiableDefaultPermissions. This object can be accessed on the Output of an Authenticator or a ClientInitializer. In addition to specific permissions, a default behaviour can be specified. The default behaviour is used when none of the defined TopicPermissions matches.

If no permissions are available the default behaviour is ALLOW. When at least one permission is added the default behaviour is DENY. The default behaviour can be changed by calling the method setDefaultBehaviour. Setting the default behaviour allows choosing between a White- or a Blacklist approach for permissions.

If no extension is installed, which adds permissions, then the default behaviour is ALLOW and all PUBLISH/SUBSCRIBE actions are authorized.

The permissions set by an extension are visible and modifiable by other extensions. Likewise permissions set in an Authenticator are also visible and modifiable in a ClientInitializer.


Topic Permissions

Each single permission for a client is represented as a TopicPermission. A TopicPermission contains a TopicFilter, a QoS level, an Activity and the information if a Publish is allowed to have the retain flag set.

Information Possible Values Default Description

Topic

Any valid MQTT Topic Filter, including wildcards (+,#)

-

The topic filter that this permission is applied for.

QoS

ZERO, ONE, TWO, ZERO_ONE, ZERO_TWO, ONE_TWO, ALL

ALL

The quality of service levels that this permission is applied for.

Activity

PUBLISH, SUBSCRIBE, ALL

ALL

The activity that this permission is applied for.

Retain

RETAINED,NOT_RETAINED,ALL

ALL

Specifies the allowed retain Flag for PUBLISH actions of this permission. Only used for MQTT PUBLISH messages.

SharedSubscription

SHARED,NOT_SHARED,ALL

ALL

Specifies if a subscription must be shared or not. Only used for MQTT SUBSCRIBE messages.

SharedGroup

A valid shared subscription group name or # to match all

#

Specifies which shared subscription group this permission is applied for. Only used for Shared Subscriptions.

Type

ALLOW,DENY

ALLOW

Specifies the behavior prompted by this permission. ALLOW authorizes for given action. DENY prevents given action.

The list of TopicPermissions is evaluated by HiveMQ in the order they are added, the first one that matches is used.


Example Usage

Add Permissions in Authenticator

The following example shows how to add a default permission to a SimpleAuthenticator.

public class MySimpleAuthenticator implements SimpleAuthenticator {

    @Override
    public void onConnect(@NotNull final SimpleAuthInput simpleAuthInput, @NotNull final SimpleAuthOutput simpleAuthOutput) {

        //Get the default permissions from the output
        final ModifiableDefaultPermissions defaultPermissions = simpleAuthOutput.getDefaultPermissions();

        //create a PUBLISH permission for topic "allowed/topic"
        final TopicPermission permission = Builders.topicPermission()
                .topicFilter("allowed/topic")
                .qos(TopicPermission.Qos.ALL)
                .activity(TopicPermission.MqttActivity.PUBLISH)
                .type(TopicPermission.PermissionType.ALLOW
                .retain(TopicPermission.Retain.ALL)
                .build();

        //add the permission to the default permissions for this client
        defaultPermissions.add(permission);

        //authenticate the client successfully
        simpleAuthOutput.authenticateSuccessfully();
    }

}


Add Client-Identifier Based Permission

This example shows how to set up a permission that allows a client to only publish/subscribe to topics that start with the same client identifier as the client. In this example, the permission is set in the ClientInitializer.

public class MyClientInitializer implements ClientInitializer {

        @Override
        public void initialize(@NotNull final InitializerInput initializerInput, @NotNull final ClientContext clientContext) {

            //Get the default permissions from the clientContext
            final ModifiableDefaultPermissions defaultPermissions = clientContext.getDefaultPermissions();

            //Get the client identifier from the input
            final String clientId = initializerInput.getClientInformation().getClientId();

            //create a permission for all topics starting with the client identifier
            final TopicPermission clientIdPermission = Builders.topicPermission()
                    .topicFilter(clientId + "/#")
                    .build();

            defaultPermissions.add(clientIdPermission);
        }
    }
When an extension starts at runtime, the ClientInitializer is also called for already connected clients. In this case, the client might already have permissions set from previous extensions.


Add Blacklist Permission

This example shows how to set up a permission that allows a client to publish/subscribe to all topics except topics that start with "admin".

public class MyClientInitializer implements ClientInitializer {

        @Override
        public void initialize(@NotNull final InitializerInput initializerInput, @NotNull final ClientContext clientContext) {

            //Get the default permissions from the clientContext
            final ModifiableDefaultPermissions defaultPermissions = clientContext.getDefaultPermissions();

            final TopicPermission adminPermission = Builders.topicPermission()
                    .topicFilter("admin/#")
                    //set the type to deny to deny every action that matches the topic filter "admin/#"
                    .type(TopicPermission.PermissionType.DENY)
                    .build();

            defaultPermissions.add(adminPermission);

            //change the default behaviour to ALLOW, so all other topics are allowed
            defaultPermissions.setDefaultBehaviour(DefaultAuthorizationBehaviour.ALLOW);
        }
    }


MQTT Outcome

When a PUBLISH or SUBSCRIBE is denied because of insufficient permissions, the following behaviour can be expected at the client side:

PUBLISH

An MQTT client is always disconnected, when it publishes to an unauthorized topic. With MQTT 5 additional information is sent to the client before disconnect.

MQTT Version QoS Behaviour

MQTT 3.1

All QoS

Connection is closed by the broker.

MQTT 3.1.1

All QoS

Connection is closed by the broker.

MQTT 5

Qos 0

DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed.

MQTT 5

Qos 1

Server responds with PUBACK packet with reason code NOT_AUTHORIZED. Then DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed.

MQTT 5

Qos 2

Server responds with PUBREC packet with reason code NOT_AUTHORIZED. Then DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed.


SUBSCRIBE

A SUBSCRIBE packet can contain multiple Subscriptions, which are individually checked by HiveMQ against the permissions. The SUBACK packet contains an individual reason code for each of those subscriptions.

MQTT Version Behaviour

MQTT 3.1

Connection is closed by the broker.

MQTT 3.1.1

SUBACK packet with reason code FAILURE for every Topic Filter that is not authorized is sent.

MQTT 5

SUBACK packet with reason code NOT_AUTHORIZED for every Topic Filter that is not authorized is sent.

When implementing an MQTT client, always check the return codes in the SUBACK packet. Only some of the Subscriptions might be authorized.


Will Publishes

A Will Publish, which is part of the CONNECT packet, is checked against the default permissions after the Authenticator and after the ClientInitializer. If a Will Publish would be sent to an unauthorized topic, then the client will not be allowed to connect and a CONNACK packet with reason code NOT_AUTHORIZED (MQTT 5) or REFUSED_NOT_AUTHORIZED (MQTT 3) will be sent by the broker. The Will Publish from this CONNECT packet is not published.



Publish Authorizer

A Publish Authorizer is an extension callback, which is called by HiveMQ for every PUBLISH packet and for every Will Publish sent by a client. The Authorizer provides a flexible way to decide if a client is allowed to publish a message on a per packet basis. In addition to the Topic the extension developer has access to other contents of the PUBLISH packet or the Will Publish.

A PublishAuthorizer can be registered with the SecurityRegistry by providing an AuthorizerProvider. An Authorizer is active for all clients. After it is registered with the Security Registry, it is also applied to already connected clients.

There are multiple outcomes that a PublishAuthorizerOuput can have:

Method Description

authorizeSuccessfully

Successfully authorize this Publish.

failAuthorization

Deny authorization for this Publish. Methods with optional reason code and reason string for PUBACK/PUBREL are available. A PUBACK/PUBREL is sent, then the client is disconnected.

disconnectClient

The client is disconnected. Methods with optional reason code and reason string for DISCONNECT are available. If this method is chosen, this Publish is denied.

nextExtensionOrDefault

No final decision is made. The next extension is called or if no next extension is available, then the default permissions are applied. If no default permissions are set, then the authorization is denied.

If the method authorizePublish returns without any of those methods called, then the Publish is denied.

An Authorizer can utilize the async output mechanism to prevent blocking other tasks from being processed. If the async output times out then the Publish is denied. If the timeout fallback SUCCESS is specified, the same behaviour as nextExtensionOrDefault is used. If the timeout fallback is FAILURE, the same behaviour as failAuthentication is used.

Default Permissions for a Publish are only evaluated, when a PublishAuthorizer calls nextExtensionOrDefault.

Example Usage

Implement a Publish Authorizer

This example shows how to implement a PublishAuthorizer with different outcomes depending on the Topic or User Properties. This Authorizer must be registered with the SecurityRegistry.

public class MyPublishAuthorizer implements PublishAuthorizer {

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

        //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();

    }
}


Implement an async Publish Authorizer

This example shows how to use the PublishAuthorizer with an async output to call an external service.

public class MyAsyncPublishAuthorizer implements PublishAuthorizer {

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

        //get the managed extension executor service
        final ManagedExtensionExecutorService extensionExecutorService = Services.extensionExecutorService();

        //make the output async with a timeout of 2 seconds
        final Async<PublishAuthorizerOutput> async = output.async(Duration.ofSeconds(2));

        //submit a task to the extension executor
        extensionExecutorService.submit(new Runnable() {
            @Override
            public void run() {

                //call an external service to decide the outcome
                final boolean result = callExternalTaks();

                if (result) {
                    output.authorizeSuccessfully();
                } else {
                    output.failAuthorization();
                }

                //Always resume the async output, otherwise it will time out
                async.resume();
            }
        });

    }
}




MQTT Outcome

When a Publish is denied by calling any of the failAuthorization methods, then the following MQTT behaviour can be expected:

MQTT Version Behaviour

MQTT 3.1, All QoS levels

Connection is closed by the broker.

MQTT 3.1.1, All QoS levels

Connection is closed by the broker.

MQTT 5, PUBLISH with QoS 0

DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed.

MQTT 5, PUBLISH with QoS 1

Broker responds with PUBACK packet with reason code NOT_AUTHORIZED. Then DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed. The reason code and reason string for the PUBACK packet can be modified in the PublishAuthorizer.

MQTT 5, PUBLISH with QoS 2

Broker responds with PUBREC packet with reason code NOT_AUTHORIZED. Then DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed. The reason code and reason string for the PUBREC packet can be modified in the PublishAuthorizer.

If the outcome disconnectClient is chosen in the PublishAuthorizer then the following behaviour can be expected:

MQTT Version Behaviour

MQTT 3.1

Connection is closed by the broker.

MQTT 3.1.1

Connection is closed by the broker.

MQTT 5

DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed. The reason code and reason string can be modified in the PublishAuthorizer.


Will Publishes

Publish Authorizers are also called for Will Publishes for every client that sends a CONNECT message containing a Will Publish. If a Will Publish is denied by calling any of the failAuthorization or disconnectClient methods, the behaviour is as follows:

MQTT Version Behaviour

MQTT 3.1

Broker responds with a CONNACK packet with reason code REFUSED_NOT_AUTHORIZED. Then the connection is closed by the broker.

MQTT 3.1.1

Broker responds with a CONNACK packet with reason code REFUSED_NOT_AUTHORIZED. Then the connection is closed by the broker.

MQTT 5

Broker responds with a CONNACK packet with reason code NOT_AUTHORIZED. Then the connection is closed by the broker. The reason code and reason string can be modified in the PublishAuthorizer.


The reason codes specified in the failAuthentication and disconnectClient method are automatically translated by HiveMQ into reason codes for a CONNACK packet. The translation is as follows.

For AckReasonCode passed in the failAuthentication methods:

AckReasonCode CONNACK Reason Code

UNSPECIFIED_ERROR

UNSPECIFIED_ERROR

IMPLEMENTATION_SPECIFIC_ERROR

IMPLEMENTATION_SPECIFIC_ERROR

NOT_AUTHORIZED

NOT_AUTHORIZED

TOPIC_NAME_INVALID

TOPIC_NAME_INVALID

PACKET_IDENTIFIER_IN_USE

UNSPECIFIED_ERROR

QUOTA_EXCEEDED

QUOTA_EXCEEDED

PAYLOAD_FORMAT_INVALID

PAYLOAD_FORMAT_INVALID

For DisconnectReasonCode passed in the disconnect methods:

DisconnectReasonCode CONNACK Reason Code

NORMAL_DISCONNECTION

UNSPECIFIED_ERROR

DISCONNECT_WITH_WILL_MESSAGE

UNSPECIFIED_ERROR

UNSPECIFIED_ERROR

UNSPECIFIED_ERROR

MALFORMED_PACKET

MALFORMED_PACKET

PROTOCOL_ERROR

PROTOCOL_ERROR

IMPLEMENTATION_SPECIFIC_ERROR

IMPLEMENTATION_SPECIFIC_ERROR

NOT_AUTHORIZED

NOT_AUTHORIZED

SERVER_BUSY

SERVER_BUSY

SERVER_SHUTTING_DOWN

SERVER_UNAVAILABLE

BAD_AUTHENTICATION_METHOD

BAD_AUTHENTICATION_METHOD

KEEP_ALIVE_TIMEOUT

UNSPECIFIED_ERROR

SESSION_TAKEN_OVER

UNSPECIFIED_ERROR

TOPIC_FILTER_INVALID

TOPIC_NAME_INVALID

TOPIC_NAME_INVALID

TOPIC_NAME_INVALID

RECEIVE_MAXIMUM_EXCEEDED

UNSPECIFIED_ERROR

TOPIC_ALIAS_INVALID

UNSPECIFIED_ERROR

PACKET_TOO_LARGE

PACKET_TOO_LARGE

MESSAGE_RATE_TOO_HIGH

UNSPECIFIED_ERROR

QUOTA_EXCEEDED

QUOTA_EXCEEDED

ADMINISTRATIVE_ACTION

UNSPECIFIED_ERROR

PAYLOAD_FORMAT_INVALID

PAYLOAD_FORMAT_INVALID

RETAIN_NOT_SUPPORTED

RETAIN_NOT_SUPPORTED

QOS_NOT_SUPPORTED

QOS_NOT_SUPPORTED

USE_ANOTHER_SERVER

USE_ANOTHER_SERVER

SERVER_MOVED

SERVER_MOVED

SHARED_SUBSCRIPTION_NOT_SUPPORTED

UNSPECIFIED_ERROR

CONNECTION_RATE_EXCEEDED

CONNECTION_RATE_EXCEEDED

MAXIMUM_CONNECT_TIME

UNSPECIFIED_ERROR

SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED

UNSPECIFIED_ERROR

WILDCARD_SUBSCRIPTION_NOT_SUPPORTED

UNSPECIFIED_ERROR

Authorizer and Default Permissions

If the Outcome of all registered Publish Authorizers is nextExtensionOrDefault then the Default Permissions are applied for this Publish.

If the last Publish Authorizer is called with the outcome nextExtensionOrDefault and no Default Permissions are set by any extension then the default behaviour is DENY and this Publish is denied, similar to an outcome of failAuthorization.



Subscription Authorizer

A Subscription Authorizer is a extension callback, which is called by HiveMQ for every Topic Filter in a SUBSCRIBE packet sent by a client. The Authorizer provides a flexible way to decide if a client is allowed to subscribe to a Topic Filter on a per Topic Filter basis. In addition to the Topic Filter the extension developer has access to other contents of the SUBSCRIBE packet, like User Properties and Subscription Options.

A SubscriptionAuthorizer can be registered with the SecurityRegistry by providing an AuthorizerProvider. An Authorizer is active for all clients. After it is registered with the Security Registry, it is also applied to already connected clients. There are multiple outcomes that a SubscriptionAuthorizerOuput can have:

Method Description

authorizeSuccessfully

Successfully authorize this Subscription.

failAuthorization

Deny authorization for this Subscription. Methods with optional reason code and reason string are available.

disconnectClient

The client is disconnected. Methods with optional reason code and reason string are available. If this mehtod is chosen, all of the Subscriptions from the same SUBSCRIBE packet are denied for the client.

nextExtensionOrDefault

No final decision is made. The next extension is called or if no next extension is available, then the default permissions are applied. If no default permissions are set, then the authorization is denied.

If the method authorizeSubscribe returns without any of those methods called, then the Subscription is denied.

An Authorizer can utilize the async output mechanism to prevent blocking other tasks from being processed. If the async output times out then the Subscription is denied. If the timeout fallback SUCCESS is specified, the same behaviour as nextExtensionOrDefault is used. If the timeout fallback is FAILURE, the same behaviour as failAuthentication is used.

Default Permissions for a Subscription are only evaluated, when a SubscriptionAuthorizer calls nextExtensionOrDefault.




Example Usage

Implement a Subscription Authorizer

This example shows how to implement a SubscriptionAuthorizer with different outcomes depending on the Topic Filter or User Properties. This Authorizer must be registered with the SecurityRegistry.

public class MySubscriptionAuthorizer implements SubscriptionAuthorizer {

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

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

            //disallow a shared subscription
            if (input.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();
        }
    }




Implement an Async Subscription Authorizer

This example shows how to use the SubscriptionAuthorizer with an async output to call an external service.

public class MyAsyncSubscriptionAuthorizer implements SubscriptionAuthorizer {

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

        //get the managed extension executor service
        final ManagedExtensionExecutorService extensionExecutorService = Services.extensionExecutorService();

        //make the output async with a timeout of 2 seconds
        final Async<SubscriptionAuthorizerOutput> async = output.async(Duration.ofSeconds(2));

        //submit a task to the extension executor
        extensionExecutorService.submit(new Runnable() {
            @Override
            public void run() {

                //call an external service to decide the outcome
                final boolean result = callExternalTaks();

                if (result) {
                    output.authorizeSuccessfully();
                } else {
                    output.failAuthorization();
                }

                //Always resume the async output, otherwise it will time out
                async.resume();
            }
        });

    }
}




MQTT Outcome

A SUBSCRIBE packet can contain multiple Subscriptions. For each of those Subscriptions the SubscriptionAuthorizer is called separately and the resulting SUBACK packet contains an individual reason code for each of those calls.

When a Subscription for a Topic Filter is denied, then the following MQTT behaviour can be expected:

MQTT Version Behaviour

MQTT 3.1

Connection is closed by the broker.

MQTT 3.1.1

SUBACK packet with reason code FAILURE for every Topic Filter that is not authorized is sent.

MQTT 5

SUBACK packet with reason code NOT_AUTHORIZED for every Topic Filter that is not authorized is sent. The reason code and reason string can be modified in the SubscriptionAuthorizer.

If the outcome disconnectClient is chosen in the SubscriptionAuthorizer then the following behaviour can be expected:

MQTT Version Behaviour

MQTT 3.1

Connection is closed by the broker.

MQTT 3.1.1

Connection is closed by the broker.

MQTT 5

DISCONNECT packet with reason code NOT_AUTHORIZED is sent by the broker and the connection is closed. The reason code and reason string can be modified in the SubscriptionAuthorizer.

When implementing an MQTT client, always check the return codes in the SUBACK packet. Only some of the Subscriptions might be authorized.

Authorizer and Default Permissions

If the outcome of all registered Subscription Authorizers is nextExtensionOrDefault then the Default Permissions are applied for this topic.

If the last SubscriptionAuthorizer is called with the outcome nextExtensionOrDefault and no Default Permissions are set by any extension then the default behaviour is DENY and this Subscription is denied, similar to an outcome of failAuthorization.