Documentation Index
Fetch the complete documentation index at: https://mintlify.com/hivemq/hivemq-community-edition/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Authorization extensions control what MQTT clients can publish and subscribe to. The Extension SDK provides fine-grained authorization for:
- Publish Authorization - Control which topics clients can publish to
- Subscribe Authorization - Control which topics clients can subscribe to
- Per-Topic Permissions - Different permissions for different topics
- Dynamic Permissions - Change permissions based on runtime conditions
Authorization Flow
Publish Authorization
- Client sends PUBLISH packet
- HiveMQ calls
PublishAuthorizer.authorizePublish() for each registered authorizer
- Authorizer allows, denies, or delegates the decision
- PUBLISH is processed or client is disconnected based on result
See PluginAuthorizerService.java:36 for publish authorization service.
Subscribe Authorization
- Client sends SUBSCRIBE packet
- HiveMQ calls
SubscriptionAuthorizer.authorizeSubscribe() for each topic filter
- Authorizer allows, denies, or modifies each subscription
- SUBACK is sent with results
See PluginAuthorizerService.java:52 for subscription authorization service.
Implementing an Authorizer
Step 1: Create AuthorizerProvider
Register your authorizer in extensionStart():
import com.hivemq.extension.sdk.api.ExtensionMain;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.parameter.*;
import com.hivemq.extension.sdk.api.services.Services;
import com.hivemq.extension.sdk.api.auth.Authorizer;
import com.hivemq.extension.sdk.api.auth.PublishAuthorizer;
import com.hivemq.extension.sdk.api.auth.SubscriptionAuthorizer;
public class MyAuthzExtension implements ExtensionMain {
@Override
public void extensionStart(
@NotNull ExtensionStartInput input,
@NotNull ExtensionStartOutput output) {
Services services = input.getServices();
// Register authorizer provider
services.authorizationService().setAuthorizerProvider(
authorizerProviderInput -> new MyAuthorizer()
);
}
@Override
public void extensionStop(
@NotNull ExtensionStopInput input,
@NotNull ExtensionStopOutput output) {
// Cleanup
}
}
Step 2: Implement PublishAuthorizer
import com.hivemq.extension.sdk.api.auth.PublishAuthorizer;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerOutput;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.packets.publish.PublishPacket;
public class MyPublishAuthorizer implements PublishAuthorizer {
@Override
public void authorizePublish(
@NotNull PublishAuthorizerInput input,
@NotNull PublishAuthorizerOutput output) {
PublishPacket publish = input.getPublishPacket();
String topic = publish.getTopic();
String clientId = input.getClientInformation().getClientId();
// Simple topic-based authorization
if (topic.startsWith("clients/" + clientId + "/")) {
// Allow clients to publish to their own topics
output.authorizeSuccessfully();
} else if (topic.startsWith("sensors/")) {
// Allow all clients to publish sensor data
output.authorizeSuccessfully();
} else {
// Deny everything else
output.failAuthorization();
}
}
}
Step 3: Implement SubscriptionAuthorizer
import com.hivemq.extension.sdk.api.auth.SubscriptionAuthorizer;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerOutput;
import com.hivemq.extension.sdk.api.packets.subscribe.Subscription;
public class MySubscriptionAuthorizer implements SubscriptionAuthorizer {
@Override
public void authorizeSubscribe(
@NotNull SubscriptionAuthorizerInput input,
@NotNull SubscriptionAuthorizerOutput output) {
Subscription subscription = input.getSubscription();
String topicFilter = subscription.getTopicFilter();
String clientId = input.getClientInformation().getClientId();
// Allow clients to subscribe to their own topics
if (topicFilter.startsWith("clients/" + clientId + "/")) {
output.authorizeSuccessfully();
}
// Allow subscribing to public topics
else if (topicFilter.startsWith("public/")) {
output.authorizeSuccessfully();
}
// Allow wildcard subscriptions for admin clients
else if (clientId.startsWith("admin-") && topicFilter.contains("#")) {
output.authorizeSuccessfully();
}
else {
output.failAuthorization();
}
}
}
Combined Authorizer
Implement both interfaces in a single class:
public class MyAuthorizer implements PublishAuthorizer, SubscriptionAuthorizer {
@Override
public void authorizePublish(
@NotNull PublishAuthorizerInput input,
@NotNull PublishAuthorizerOutput output) {
// Publish authorization logic
}
@Override
public void authorizeSubscribe(
@NotNull SubscriptionAuthorizerInput input,
@NotNull SubscriptionAuthorizerOutput output) {
// Subscribe authorization logic
}
}
Authorization Methods
Allow
Grant permission for the operation:
output.authorizeSuccessfully();
Deny
Deny permission for the operation:
output.failAuthorization();
For MQTT 5.0, optionally specify a reason code:
import com.hivemq.extension.sdk.api.packets.general.DisconnectReasonCode;
output.failAuthorization(
DisconnectReasonCode.NOT_AUTHORIZED,
"Insufficient permissions for topic"
);
Delegate
Pass decision to next authorizer:
output.nextExtensionOrDefault();
Disconnect Client
Forcefully disconnect the client:
output.disconnectClient(
DisconnectReasonCode.NOT_AUTHORIZED,
"Policy violation"
);
Modifying Subscriptions
Change subscription properties during authorization:
@Override
public void authorizeSubscribe(
@NotNull SubscriptionAuthorizerInput input,
@NotNull SubscriptionAuthorizerOutput output) {
Subscription subscription = input.getSubscription();
ModifiableSubscription modifiable = output.getSubscription();
// Downgrade QoS for non-premium clients
String clientId = input.getClientInformation().getClientId();
if (!clientId.startsWith("premium-")) {
modifiable.setQos(Qos.AT_MOST_ONCE);
}
// Allow subscription with modifications
output.authorizeSuccessfully();
}
Role-Based Authorization
Implement role-based access control:
import java.util.*;
public class RoleBasedAuthorizer implements PublishAuthorizer, SubscriptionAuthorizer {
private final Map<String, Set<String>> userRoles;
private final Map<String, Set<String>> rolePublishPermissions;
private final Map<String, Set<String>> roleSubscribePermissions;
public RoleBasedAuthorizer() {
// Initialize role mappings
userRoles = new HashMap<>();
userRoles.put("admin", Set.of("admin", "user"));
userRoles.put("sensor-001", Set.of("sensor"));
// Define role permissions
rolePublishPermissions = new HashMap<>();
rolePublishPermissions.put("admin", Set.of("#"));
rolePublishPermissions.put("sensor", Set.of("sensors/#"));
rolePublishPermissions.put("user", Set.of("public/#"));
roleSubscribePermissions = new HashMap<>();
roleSubscribePermissions.put("admin", Set.of("#"));
roleSubscribePermissions.put("user", Set.of("public/#", "notifications/#"));
}
@Override
public void authorizePublish(
@NotNull PublishAuthorizerInput input,
@NotNull PublishAuthorizerOutput output) {
String clientId = input.getClientInformation().getClientId();
String topic = input.getPublishPacket().getTopic();
Set<String> roles = userRoles.getOrDefault(clientId, Collections.emptySet());
for (String role : roles) {
Set<String> permissions = rolePublishPermissions.getOrDefault(role, Collections.emptySet());
for (String pattern : permissions) {
if (topicMatches(topic, pattern)) {
output.authorizeSuccessfully();
return;
}
}
}
output.failAuthorization("No permission to publish to " + topic);
}
@Override
public void authorizeSubscribe(
@NotNull SubscriptionAuthorizerInput input,
@NotNull SubscriptionAuthorizerOutput output) {
String clientId = input.getClientInformation().getClientId();
String topicFilter = input.getSubscription().getTopicFilter();
Set<String> roles = userRoles.getOrDefault(clientId, Collections.emptySet());
for (String role : roles) {
Set<String> permissions = roleSubscribePermissions.getOrDefault(role, Collections.emptySet());
for (String pattern : permissions) {
if (topicMatches(topicFilter, pattern)) {
output.authorizeSuccessfully();
return;
}
}
}
output.failAuthorization("No permission to subscribe to " + topicFilter);
}
private boolean topicMatches(String topic, String pattern) {
// Simple wildcard matching (# and +)
if (pattern.equals("#")) return true;
String[] topicLevels = topic.split("/");
String[] patternLevels = pattern.split("/");
for (int i = 0; i < patternLevels.length; i++) {
if (patternLevels[i].equals("#")) return true;
if (i >= topicLevels.length) return false;
if (!patternLevels[i].equals("+") && !patternLevels[i].equals(topicLevels[i])) {
return false;
}
}
return topicLevels.length == patternLevels.length;
}
}
Async Authorization
Perform authorization checks asynchronously:
import com.hivemq.extension.sdk.api.async.Async;
import com.hivemq.extension.sdk.api.async.TimeoutFallback;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
public class AsyncAuthorizer implements PublishAuthorizer {
private final AuthorizationService authService;
@Override
public void authorizePublish(
@NotNull PublishAuthorizerInput input,
@NotNull PublishAuthorizerOutput output) {
// Enable async mode
Async<PublishAuthorizerOutput> async = output.async(
Duration.ofSeconds(3),
TimeoutFallback.FAILURE
);
String clientId = input.getClientInformation().getClientId();
String topic = input.getPublishPacket().getTopic();
// Async permission check
authService.checkPermissionAsync(clientId, "publish", topic)
.thenAccept(allowed -> {
if (allowed) {
async.resume().authorizeSuccessfully();
} else {
async.resume().failAuthorization();
}
})
.exceptionally(error -> {
// Fail securely on errors
async.resume().failAuthorization("Authorization error");
return null;
});
}
}
Database-Backed Authorization
Load permissions from a database:
import java.sql.*;
import java.util.concurrent.CompletableFuture;
public class DatabaseAuthorizer implements PublishAuthorizer, SubscriptionAuthorizer {
private final String jdbcUrl;
public DatabaseAuthorizer(String jdbcUrl) {
this.jdbcUrl = jdbcUrl;
}
@Override
public void authorizePublish(
@NotNull PublishAuthorizerInput input,
@NotNull PublishAuthorizerOutput output) {
Async<PublishAuthorizerOutput> async = output.async(
Duration.ofSeconds(5),
TimeoutFallback.FAILURE
);
String clientId = input.getClientInformation().getClientId();
String topic = input.getPublishPacket().getTopic();
CompletableFuture.supplyAsync(() ->
checkPublishPermission(clientId, topic)
).thenAccept(allowed -> {
if (allowed) {
async.resume().authorizeSuccessfully();
} else {
async.resume().failAuthorization();
}
});
}
@Override
public void authorizeSubscribe(
@NotNull SubscriptionAuthorizerInput input,
@NotNull SubscriptionAuthorizerOutput output) {
Async<SubscriptionAuthorizerOutput> async = output.async(
Duration.ofSeconds(5),
TimeoutFallback.FAILURE
);
String clientId = input.getClientInformation().getClientId();
String topicFilter = input.getSubscription().getTopicFilter();
CompletableFuture.supplyAsync(() ->
checkSubscribePermission(clientId, topicFilter)
).thenAccept(allowed -> {
if (allowed) {
async.resume().authorizeSuccessfully();
} else {
async.resume().failAuthorization();
}
});
}
private boolean checkPublishPermission(String clientId, String topic) {
String sql = "SELECT COUNT(*) FROM publish_acl WHERE client_id = ? AND topic_pattern = ?";
try (Connection conn = DriverManager.getConnection(jdbcUrl);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, clientId);
stmt.setString(2, topic);
ResultSet rs = stmt.executeQuery();
return rs.next() && rs.getInt(1) > 0;
} catch (SQLException e) {
e.printStackTrace();
return false; // Fail securely
}
}
private boolean checkSubscribePermission(String clientId, String topicFilter) {
String sql = "SELECT COUNT(*) FROM subscribe_acl WHERE client_id = ? AND topic_pattern = ?";
try (Connection conn = DriverManager.getConnection(jdbcUrl);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, clientId);
stmt.setString(2, topicFilter);
ResultSet rs = stmt.executeQuery();
return rs.next() && rs.getInt(1) > 0;
} catch (SQLException e) {
e.printStackTrace();
return false; // Fail securely
}
}
}
Default Permissions
Set default permissions for clients using Client Initializers:
import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer;
import com.hivemq.extension.sdk.api.client.ClientContext;
import com.hivemq.extension.sdk.api.client.parameter.InitializerInput;
import com.hivemq.extension.sdk.api.packets.auth.DefaultPermissions;
public class DefaultPermissionsInitializer implements ClientInitializer {
@Override
public void initialize(
@NotNull InitializerInput input,
@NotNull ClientContext context) {
String clientId = input.getClientInformation().getClientId();
DefaultPermissions permissions = context.getDefaultPermissions();
// Allow publishing to own client topics
permissions.add(
DefaultPermissions.publish("clients/" + clientId + "/#")
);
// Allow subscribing to own client topics
permissions.add(
DefaultPermissions.subscribe("clients/" + clientId + "/#")
);
// Allow all clients to subscribe to public topics
permissions.add(
DefaultPermissions.subscribe("public/#")
);
}
}
See Authorizers.java:45 for authorizer provider registration.
Best Practices
Security
- Fail Securely - Deny access on errors rather than allowing
- Validate Topic Patterns - Ensure topic filters don’t grant excessive access
- Audit Authorization - Log authorization decisions for security auditing
- Principle of Least Privilege - Grant minimum necessary permissions
- Separate Read/Write - Use different permissions for publish and subscribe
- Use Async Mode - Don’t block for permission lookups
- Cache Permissions - Cache frequently checked permissions
- Optimize Database Queries - Index ACL tables properly
- Minimize Latency - Keep authorization fast to reduce message latency
Maintainability
- Centralize Permission Logic - Don’t duplicate authorization rules
- Use Configuration - Externalize permission rules when possible
- Document Permissions - Clearly document permission model
- Test Thoroughly - Test all permission scenarios
Testing Authorization
Test authorization with MQTT clients:
# Test publish authorization
mosquitto_pub -h localhost -u sensor-001 \
-t sensors/temperature -m "23.5"
# Test subscribe authorization
mosquitto_sub -h localhost -u admin \
-t "#" -v
# Expect authorization failure
mosquitto_pub -h localhost -u sensor-001 \
-t admin/config -m "hack"
Troubleshooting
Authorization Always Fails
- Verify authorizer provider is registered
- Check
authorizeSuccessfully() is called for allowed operations
- Review topic matching logic
- Enable debug logging for authorization
Authorization Not Applied
- Ensure extension is loaded and started
- Check extension priority (lower numbers execute first)
- Verify no other authorizers are overriding decisions
- Use async mode for I/O operations
- Implement permission caching
- Optimize database queries
- Review authorization latency metrics
See PluginAuthorizerService.java:28 for internal authorization service interface.
Next Steps
Authentication
Implement custom authentication
Client Initializers
Set default permissions and initialize clients
Packet Interceptors
Intercept and modify MQTT packets
Extension SDK
Learn more about the Extension SDK