Authorization
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 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 a default permission is added in 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 permission for topic "allowed/topic"
final TopicPermission permission = Builders.topicPermission()
.topicFilter("allowed/topic")
.qos(TopicPermission.Qos.ALL)
.activity(TopicPermission.MqttActivity.ALL)
.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 Permissions for own client identifier
Example implementation of a permission, which allows a client to only publish/subscribe to topics starting with the client’s identifier. The permission is set in the ClientInitializer in this example.
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);
}
}
If an extension is started at runtime, then 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
Example implementation of a permission setup, which allows a client to publish/subscribe to all topics except topics starting 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 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
|
|
|
|
If the outcome disconnectClient
is chosen in the PublishAuthorizer then the following behaviour can be expected:
MQTT Version | Behaviour |
---|---|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For DisconnectReasonCode
passed in the disconnect
methods:
DisconnectReasonCode | CONNACK Reason Code |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
|
|
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 |
---|---|
|
|
|
|
|
|
If the outcome disconnectClient
is chosen in the SubscriptionAuthorizer then the following behaviour can be expected:
MQTT Version | Behaviour |
---|---|
|
|
|
|
|
|
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
.