Authentication and Authorization

One of the many use cases for writing a HiveMQ plugin is the implementation of client authentication and authorization. The callbacks enables the plugin developer among other things to completely customize the authentication and authorization behavior.

Client Authentication

The following sequence diagram shows an overview of what happens in the HiveMQ core, when a new client is trying to connect and how a plugin can interfere with it.

Execution flow of callbacks during and after the client authentication

Client Authentication Callbacks

Default Behavior
When no plugin is present, all clients are authenticated successfully.

Implement username/password authentication

Example of an username/password authentication callback
public class UserAuthentication implements OnAuthenticationCallback {

    Logger log = LoggerFactory.getLogger(UserAuthentication.class);

    @Override
    public Boolean checkCredentials(ClientCredentialsData clientData) throws AuthenticationException { (1)


        String username;
        if (!clientData.getUsername().isPresent()) { (4)
            throw new AuthenticationException("No Username provided", ReturnCode.REFUSED_NOT_AUTHORIZED); (2)
        }
        username = clientData.getUsername().get();


        if (Strings.isNullOrEmpty(username)) {
            throw new AuthenticationException("No Username provided", ReturnCode.REFUSED_NOT_AUTHORIZED); (2)
        }

        Optional<String> password = Optional.fromNullable(retrievePasswordFromDatabase(username));

        if (!password.isPresent()) {
            throw new AuthenticationException("No Account with the credentials was found!", ReturnCode.REFUSED_NOT_AUTHORIZED); (2)
        } else {
            if (clientData.getPassword().get().equals(password.get())) {
                return true;
            }
            return false;
        }

    }

    @Cached(timeToLive = 10, timeUnit = TimeUnit.MINUTES) (3)
    private String retrievePasswordFromDatabase(String username) {

        String password =....     //Call to any database to ask for the password of the user

        return password;
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
1 ClientData holds all data provided by the client and can be used to identify it.
2 If an AuthenticationException is thrown, the client will be disconnected independently of possible other authentication plugins, see the guidelines for multiple plugins.
3 The @Cached annotation is used to cache the request for a particular username for 10 minutes, therefore the plugin only blocks when it fetches the value from the database.
4 ClientData uses the Optional class from Google Guava, which allows better handling of possible null values.

Client Authorization

The authorization of is verified every time a client tries to publish to a certain topic or subscribes to topics. The following diagram shows all possible flows of a publish message (the same flow applies for a subscribe message):

Client Authorization Callback

The most interesting callback here is the OnAuthorizationCallback, which returns a list of `MqttTopicPermission`s for the client.

The list of `MqttTopicPermission`s should contains all permissions the client has. The matching if a certain action will be allowed due to the permissions of the client will be done by HiveMQ.

Additionally a default authorization behavior has to be specified. Possibilities are: ACCEPT, DENY and NEXT. NEXT should be used if multiple OnAuthorizationCallback s are registered to signal that another callback can set the result. If all callbacks return NEXT the action is not authorized.

The following snippet shows the available constructors for the MqttTopicPermission class.

Available constructors for MqttTopicPermission
MqttTopicPermission(final String topic, final TYPE type) (1)

MqttTopicPermission(final String topic, final TYPE type, final ACTIVITY activity) (2)

MqttTopicPermission(final String topic, final TYPE type, final QOS qos) (3)

MqttTopicPermission(final String topic, final TYPE type, final QOS qos, final ACTIVITY activity) (4)

MqttTopicPermission(final String topic, final TYPE type, final QOS qos, final ACTIVITY activity, final RETAIN publishRetain) (5)
1 Allow or deny the topic a client can publish/subscribe to, with all Quality of Service (QoS) levels.
2 Allow or deny the topic and the client’s ability to publish, subscribe or do both, for all QoS.
3 Allow or deny the topic and the client’s ability to use only some of the QoS or all of them, allow or deny publish and subscribe.
4 Allow or deny the topic, the client’s ability to use QoS and to subscribe/publish.
5 Allow or deny the topic, the client’s ability to use QoS and to subscribe/publish, and the ability to send retained messages.

Client Authorization Example

Example implementation of a permission, which allows a client to only publish/subscribe to topics with his client id upfront.
public class ClientIdTopic implements OnAuthorizationCallback {

    @Override
    @Cached(timeToLive = 10, timeUnit = TimeUnit.MINUTES) (3)
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        List<MqttTopicPermission> mqttTopicPermissions = new ArrayList<>();
        mqttTopicPermissions.add(new MqttTopicPermission(clientData.getClientId() + "/#", MqttTopicPermission.TYPE.ALLOW)); (1)

        return mqttTopicPermissions;

    }

    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.DENY; (2)
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
1 The permission allows a client only to publish/subscribe to topics, which begin with his client id.
2 The client is not allowed to publish/subscribe to any other topic.
3 The method is another ideal candidate for the @Cached annotation.

Black- and Whitelists

Blacklist example

Example blacklist implementation:
public class BlacklistAuthorisation implements OnAuthorizationCallback {

    @Override
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        final List<MqttTopicPermission> permissions = new ArrayList<>();
        permissions.add(new MqttTopicPermission("client/" + clientData.getClientId(), TYPE.ALLOW, QOS.ONE)); (1)
        permissions.add(new MqttTopicPermission("client/#", TYPE.DENY, ACTIVITY.SUBSCRIBE)); (2)

        return permissions;
    }


    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.ACCEPT; (3)
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
1 Subscriptions for topic "client/<client id>" with QoS 1, are allowed.
2 Subscriptions for all other topics that start with "client/" are denied.
3 Everything else is allowed.

Whitelist example

Example whitelist implementation:
public class WhitelistAuthorisation implements OnAuthorizationCallback {

    @Override
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        final List<MqttTopicPermission> permissions = new ArrayList<>();
                permissions.add(new MqttTopicPermission("client/" + clientData.getClientId(), TYPE.ALLOW, QOS.ONE, ACTIVITY.SUBSCRIBE)); (1)
                permissions.add(new MqttTopicPermission("client/#", TYPE.ALLOW, QOS.ALL, ACTIVITY.PUBLISH, RETAIN.NOT_RETAINED)); (2)

        return permissions;
    }


    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.DENY; (3)
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
1 Subscriptions for topic "client/<client id>" with QoS 1, are allowed.
2 Publishes for topics starting with "client/" are allowed. As long as they are not retained.
3 Everything else is denied. AuthorizationBehaviour.NEXT would be possible as well.

Topic Permission Order

Keep in mind that when authorizing a desired action by a client (publish or subscribe) against TopicPermissions, the first permission that matches the used topic will be used. Other TopicPermissions in the list that might match the given topic will be ignored.

Examples

Example 1:
public class TopicPermissionOrderExample1 implements OnAuthorizationCallback {

    @Override
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        final List<MqttTopicPermission> permissions = new ArrayList<>();

                // Allow access to "client/+/status" topics with QOS 1
                permissions.add(new MqttTopicPermission("client/+/status", TYPE.ALLOW, QOS.ONE));

                // This permission would allow all QOS levels but is ignored because the first permission already matches the same topic path.
                permissions.add(new MqttTopicPermission("client/+/status", TYPE.ALLOW, QOS.ALL));

        return permissions;
    }

    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.DENY;
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
Example 2:
public class TopicPermissionOrderExample2 implements OnAuthorizationCallback {

    @Override
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        final List<MqttTopicPermission> permissions = new ArrayList<>();

                // Allow access to "client/+/status" topics
                permissions.add(new MqttTopicPermission("client/+/status", TYPE.ALLOW));

                // Deny access to topics that start with "client/"
                permissions.add(new MqttTopicPermission("client/#", TYPE.DENY));

                // In this order the client is allowed to access "client/+/status/ topics.
                // If you swap these permissions you will not have the same effect.
                // The "deny" rule would be read first and any other following permissions would be ignored.
                // Example 3 will demonstrate this.

        return permissions;
    }

    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.DENY;
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}
Example 3:
public class TopicPermissionOrderExample3 implements OnAuthorizationCallback {

    @Override
    public List<MqttTopicPermission> getPermissionsForClient(ClientData clientData) {

        final List<MqttTopicPermission> permissions = new ArrayList<>();

                // Deny access to topics that start with "client/"
                permissions.add(new MqttTopicPermission("client/#", TYPE.DENY));

                // This permission would allow access to "client/+/status" topics but is ignored because the first permission matches already to all topics that start with 'client/'
                permissions.add(new MqttTopicPermission("client/+/status", TYPE.ALLOW));

        return permissions;
    }

    @Override
    public AuthorizationBehaviour getDefaultBehaviour() {
        return AuthorizationBehaviour.DENY;
    }

    @Override
    public int priority() {
        return CallbackPriority.MEDIUM;
    }
}

Guidelines for multiple plugins

In the case more than one authentication or authorization plugins is running simultaneously, additional aspects have to be taken into consideration.

AuthenticationException or false

There are two ways in a OnAuthenticationCallback to tell HiveMQ that the client is not allowed to connect:

  • return false

  • thrown an AuthenticationException

If there is only a single plugin implementing the OnAuthenticationCallback the only reason to throw an AuthenticationException is to customize the return code. In a multiple plugin scenario there is another important difference: When an AuthenticationException is being catched by HiveMQ, the result of the other plugins have no meaning, because the client gets disconnected instantly. By contrast, when returning false, HiveMQ synchronizes all callbacks and then computes the over all result. More on that in the next chapter.

How to decide if AuthenticationException or return false should be used?
This is up to the developer, because it is highly depending on the authentication logic in a particular use case. See the following example for an advice.

An example use case to show the difference would be, that the client user can be either in a MySQL or PostgreSQL database. So there are two plugins, one for checking the existence of the user in each database. If one of the plugins encounter that the client has not provided username and password, an AuthenticationException should be thrown, because most likely without this information the other plugin is also not able to authenticate.

Difference between returning false and throwing an AuthenticationException
@Override
@Cached(timeToLive = 1, timeUnit = TimeUnit.MINUTES)
public Boolean checkCredentials(ClientCredentialsData clientData) throws AuthenticationException {


    String clientUsername;
    String clientPassword;
    if (!clientData.getUsername().isPresent() && !clientData.getPassword().isPresent() ) {
        clientUsername = clientData.getUsername().get();
        clientPassword = clientData.getPassword().get();
    }
    else
    {
        throw new AuthenticationException(ReturnCode.REFUSED_BAD_USERNAME_OR_PASSWORD);(1)
    }

    String savedPassword = retrievePassword(clientUsername);

    if (clientPassword.equals(savedPassword))
    {
        return true;
    }
    else
    {
        return false; (2)
    }
}
1 Username and password are not present and an AuthenticationException is thrown, because in the above stated use case no authentication can be granted.
2 If the user is not present or the password does not match, returning false would be the correct thing to do, preserving the chance for the other plugin to successfully authenticate the client.

Successful authentication/authorization

If none of the plugins had thrown an AuthenticationException, the return value of each plugin will be taken into consideration for determine the overall result.

Criterion to authentication or authorize a client
If one of the plugins returned true or the matching permission, HiveMQ is authenticating the client or accepting the authorization request.

Unsuccessful authorization

If an action attempted by a client is not successfully authorized the behavior differs between actions and MQTT protocol versions.

In case of all PUBLISH actions and SUBSCRIBE actions using the MQTT 3.1 protocol the client suffers a hard disconnect.

In case of SUBSCRIBE actions using the MQTT 3.1.1 protocol the client remains connected and receives an SUBACK message containing the code 128 (Failure) for every unauthorized topic.

In some clients, like eclipse paho, you have to manually check the returned SUBACK codes.