HiveMQ Enterprise Security Extension Customization SDK

The Enterprise Security Extension Customization SDK allows you to programmatically specify sophisticated custom handling as part of the ESE authentication and authorization processing pipeline.

Features

The Enterprise Security Extension Customization SDK gives you fine-grained control over the content of ESE variables.

Common Preprocessor:

MQTT Preprocessor:

Requirements

Quick Start with the Customization SDK

The Enterprise Security Extension Customization SDK uses the same Input/Output and async principles as the HiveMQ Extension SDK.

The quickest way to learn about the customization SDK is to check out our Hello World Customization project on GitHub and use it as the basis for your own customization.

The hivemq-enterprise-security-hello-world-customization project provides examples to get you started with the Enterprise Security Extension Customization SDK:

  • The IpAllowlistMqttPreprocessor that adds IP allowlist checking as an additional security layer for MQTT client authentication.

  • The ExternalRolesCommonPreprocessor that makes roles from an external HTTP server available for authorization.

Deploying A Customization

You can deploy your customization as follows:

  1. Add the fully qualified class name of your custom preprocessor to your extension configuration.

  2. Run the ./gradlew jar task from your Gradle project to build your customization.
    For example, use the task from the Hello World project.

  3. Copy the jar file from your local build/libs folder to the HIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations directory of your HiveMQ installation.

  4. Start the HiveMQ broker.

Customization Metrics

When you deploy a custom preprocessor, the extension adds dedicated metrics to your HiveMQ broker. Monitor these metrics to gain valuable insights into the behavior of your preprocessor over time.

For more information, see Custom Preprocessor Metrics.

In addition to the standard metrics, the Customization SDK metrics registry allows you to create custom metrics to fulfill your individual business needs.

For more information, see Custom Metrics.

Custom Preprocessor Configuration

You can find all of the details regarding configuration in the custom preprocessor section of the Enterprise Security Extension documentation.

Common Preprocessor

The common preprocessor is a custom preprocessor that lets you extend the capabilities of any security pipeline. Implement this preprocessor to programmatically access and process ESE variables, optionally asynchronously, and write them back to be picked up in later stages of the security pipeline.

Example Common Preprocessor configuration
public class CommonPreprocessorImpl implements CommonPreprocessor {
    @Override
    public void init(final @NotNull CommonPreprocessorInitInput input) {
        // Implement your init logic here. (optional)
    }

    @Override
    public void process(
            final @NotNull CommonPreprocessorProcessInput input,
            final @NotNull CommonPreprocessorProcessOutput output) {
        // Implement your processing logic here.
    }

    @Override
    public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
        // Implement your shutdown logic here. (optional)
    }
}

MQTT Preprocessor

The MQTT preprocessor is a custom preprocessor that lets you extend the capabilities of an MQTT listener pipeline. This preprocessor has the same capabilities as the common preprocessor, plus access to MQTT client-specific information.

Example MQTT Preprocessor configuration
public class MqttPreprocessorImpl implements MqttPreprocessor {
    @Override
    public void init(final @NotNull MqttPreprocessorInitInput input) {
        // Implement your init logic here. (optional)
    }

    @Override
    public void process(
            final @NotNull MqttPreprocessorProcessInput input,
            final @NotNull MqttPreprocessorProcessOutput output) {
        // Implement your processing logic here.
    }

    @Override
    public void shutdown(final @NotNull MqttPreprocessorShutdownInput input) {
        // Implement your shutdown logic here. (optional)
    }
}
Configuring an MQTT preprocessor is only allowed on MQTT listener pipelines.

MQTT Client-specific Information

The MQTT preprocessor, allows you to access MQTT client-specific information such as the client IP or TLS connection details during processing.

Example configuration to access MQTT client-specific Information
public class MqttClientSpecificInformationPreprocessorImpl implements MqttPreprocessor {
    @Override
    public void process(
            final @NotNull MqttPreprocessorProcessInput input,
            final @NotNull MqttPreprocessorProcessOutput output) {
        final ConnectionInformation connectionInformation = input.getConnectionInformation();
        final ClientInformation clientInformation = input.getClientInformation();
        final ConnectPacket connectPacket = input.getConnectPacket();
    }
}

Custom Preprocessor Lifecycle

Custom preprocessor implementations referenced in the custom preprocessor configuration are dynamically loaded, instantiated, and initialized when the extension is launched. After these steps, the preprocessor is ready for processing as part of the surrounding pipeline. The preprocessor shuts down when the extension stops.

The preprocessor executes the following steps:

  1. Read the fully qualified class name from the custom preprocessor configuration <implementation> tag.

  2. Load the class by scanning all jar files contained in the HIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations folder, including subfolders. Your custom preprocessor can access all classes in the jars because they are all part of the same ClassLoader.

  3. Create the custom preprocessor instance by calling the mandatory public no arg constructor.

  4. Call the init method once.

  5. Call the process method continuously as part of the security pipeline.

  6. Call the shutdown method once when the extension stops. The shutdown method is also called if the init method is not successful and the extension stops.

Your compiled implementation (.class files) of the custom preprocessors and all associated dependencies must be placed in java archives (.jar) in the HIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations folder or subfolders.
Multiple threads call the process method concurrently. To ensure proper processing, your implementation must be thread-safe.

If an error occurs during the initialization of a custom preprocessor, the extension stops. For example, if the class is not found or the init method throws an Exception.

If the process method throws an Exception the com.hivemq.extensions.ese.custom.preprocessor.process.failed metric is incremented, an error is logged, and access is denied to the accessing client.

Any Exception thrown by the shutdown is logged but not handled further.

We do not encourage throwing exceptions from custom preprocessor methods. All exceptions should be handled inside the methods of your implementation.

To troubleshoot your custom preprocessor, monitor the Custom Preprocessor Metrics and consult the hivemq.log file.

ESE Variables

ESE variables are an essential concept of the Enterprise Security Extension and are used to pass information between pipeline steps. The Customization SDK gives you full access to the variables by providing APIs to read and write their content.

Example to access ESE Variables
public class EseVariableCommonPreprocessorImpl implements CommonPreprocessor {
    @Override
    public void process(
            final @NotNull CommonPreprocessorProcessInput input,
            final @NotNull CommonPreprocessorProcessOutput output) {
        input.getEseVariablesInput().getAuthenticationKey().ifPresent(authenticationKey -> {
            if (!authenticationKey.startsWith("prefix-")) {
                output.getEseVariablesOutput().setAuthenticationKey("prefix-" + authenticationKey);
            }
        });
    }
}
  1. Read the content of the ESE variable authentication-key.

  2. Check if the ESE variable is present and does not start with prefix-

  3. If true, add the missing prefix and write the new value back to the ESE variable authentication-key.

The updated output is taken into account as soon as the synchronous or asynchronous processing of the custom preprocessor is finished. The content of an ESE variable can be removed by setting the value to null.

Asynchronous Processing

Custom preprocessors are allowed to access external resources in a non-blocking/asynchronous manner only. For example, to make HTTP requests :

Example asynchronous processing implementation:
public class AsyncCommonPreprocessorImpl implements CommonPreprocessor {

    private final @NotNull HttpClient httpClient = HttpClient.newHttpClient();

    @Override
    public void process(
            final @NotNull CommonPreprocessorProcessInput input,
            final @NotNull CommonPreprocessorProcessOutput output) {
        final Async<CommonPreprocessorProcessOutput> async = output.async();
        try {
            final HttpRequest request = HttpRequest.newBuilder().build();
            httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) //
                    .thenAccept(response -> {
                        // Implement your response handling here e.g. updating ESE variables.
                    }).exceptionally(throwable -> {
                        // Implement your error handling here.
                        return null;
                    })
                    // Always call async.resume() when finished.
                    .thenRun(async::resume);
        } catch (final Exception e) {
            // Implement your error handling here.
            // Always call async.resume() when finished.
            async.resume();
        }
    }
}
  1. Call output.async() before starting the asynchronous processing. This will make the rest of the pipeline wait until async.resume() is called.

  2. Use non-blocking IO to retrieve external data and, if applicable, consider caching.

  3. Update the ESE variables for further processing in the pipeline.

  4. In any case call async.resume(). This can be when an Exception was thrown before the asynchronous block even started or when the asynchronous block finished with success or failure.

The output.async() method can only be called once per process call.
Once the output.async() method is called, the custom preprocessor must call the async.resume() method when the successful or unsuccessful processing finishes.

Custom Settings

The custom preprocessor configuration allows you to externalize your configuration via <custom-settings>. These settings can be accessed via SDK:

Example to access custom settings
public class CustomSettingsCommonPreprocessorImpl implements CommonPreprocessor {

    private @Nullable String customValue;

    @Override
    public void init(final @NotNull CommonPreprocessorInitInput input) {
        // Accessible in init
        final CustomSettings customSettings = input.getCustomSettings();

        // Store for later use in process and/or shutdown.
        customValue = customSettings.getFirst("configured-setting").orElse(null);
    }

    @Override
    public void process(
            final @NotNull CommonPreprocessorProcessInput input,
            final @NotNull CommonPreprocessorProcessOutput output) {
        // Accessible in process
        final CustomSettings customSettings = input.getCustomSettings();
    }

    @Override
    public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
        // Accessible in shutdown
        final CustomSettings customSettings = input.getCustomSettings();
    }
}

Custom Metrics

The Customization SDK allows you to create and update custom metrics via the MetricRegistry.

Example custom metrics configuration
public class CustomMetricsCommonPreprocessorImpl implements CommonPreprocessor {

    private @NotNull Counter counter;

    @Override
    public void init(final @NotNull CommonPreprocessorInitInput input) {
        // Accessible in init
        final MetricRegistry metricRegistry = input.getMetricRegistry();

        // Store for later use in process and/or shutdown.
        counter = metricRegistry.counter("my.custom.Counter");
    }

    @Override
    public void process(
            final @NotNull CommonPreprocessorProcessInput input,
            final @NotNull CommonPreprocessorProcessOutput output) {
        // Accessible in process
        final MetricRegistry metricRegistry = input.getMetricRegistry();
    }

    @Override
    public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
        // Accessible in shutdown
        final MetricRegistry metricRegistry = input.getMetricRegistry();
    }
}