Edit Page

Interceptors RESTHeart

Interceptors play a crucial role in the RESTHeart framework, allowing developers to observe and modify requests and responses throughout the request lifecycle. This document provides an overview of the Interceptor class, available interceptor interfaces, and usage examples.

The Interceptor Class

An Interceptor is comprised of a class implementing one of the Interceptor interfaces, such as MongoInterceptor, and annotated with @RegisterPlugin.

The Interceptor interface encompasses two pivotal methods: handle(req, res) and resolve(req, res). The paramount method is handle(req, res), which is triggered only when resolve(req, res) evaluates to true. This design ensures that the interceptor’s logic is selectively applied, allowing developers to execute customized actions precisely when conditions specified in the resolve() method are met.

The req and res arguments allow you to retrieve and modify the content, query parameters, and headers of both the request and response objects.

The special interceptor interface, WildcardInterceptor, intercepts requests handled by any Service. Other interceptors can handle requests with matching types between the Service and the Interceptor:

  • WildcardInterceptor: Intercepts requests handled by any service

  • ByteArrayInterceptor: Intercepts requests handled by services implementing ByteArrayService

  • JsonInterceptor: Intercepts requests handled by services implementing JsonService

  • BsonInterceptor: Intercepts requests handled by services implementing BsonService

  • MongoInterceptor: Intercepts requests handled by the MongoService

  • GraphQLInterceptor: Intercepts GraphQL requests (available since RESTHeart v7)

  • ProxyInterceptor: Intercepts proxied requests

Note
MongoInterceptor is particularly useful as it allows intercepting requests to the MongoService, adding logic to its data API. For instance, the following response interceptor removes the property secret from GET /coll.
Note
GraphQLInterceptor became available in RESTHeart v7 when GraphQLRequest was moved to restheart-commons, enabling plugin developers to intercept and modify GraphQL requests and responses. This allows implementing custom logic such as query validation, result transformation, or access logging for GraphQL operations.
@RegisterPlugin(name = "secretFilter",
    interceptPoint = InterceptPoint.RESPONSE,
    description = "removes the property 'secret' from GET /coll")
public class ReadOnlyPropFilter implements MongoInterceptor {
    @Override
    public void handle(MongoRequest request, MongoResponse response) throws Exception {
        if (response.getContent().isDocument()) {
            response.getContent().asDocument().remove("secret");
        } else if (request.getContent().isArray()) {
            response.getContent().asArray().stream()
                .map(doc -> doc.asDocument())
                .forEach(doc -> doc.remove("secret"));
        }
    }

    @Override
    public boolean resolve(MongoRequest request, MongoResponse response) {
        return request.isGet()
            && response.getContent() != null
            && "coll".equalsIgnoreCase(request.getCollectionName());
    }
}

@RegisterPlugin Annotation

All plugins, including Interceptors, must be annotated with @RegisterPlugin. This annotation serves two primary purposes:

  • Discovery: Allows RESTHeart to discover plugin implementation classes in deployed JARs (see How to Deploy Plugins).

  • Configuration: Specifies parameters such as the URI of a service or the intercept point of an interceptor.

Example:

@RegisterPlugin(name = "foo",
    description = "just an example service",
    defaultUri="/foo",      // optional, default /<service-name>
    secure=false,           // optional, default false
    enabledByDefault=false) // optional, default true
public class MyPlugin implements JsonService {
...
}

Annotation Parameters:

param description mandatory default value

name

the name of the Interceptor

yes

none

description

description of the Interceptor

yes

none

enabledByDefault

true to enable the plugin; can be overridden by the plugin configuration option enabled

no

true

interceptPoint

the intercept point: REQUEST_BEFORE_EXCHANGE_INIT, REQUEST_BEFORE_AUTH, REQUEST_AFTER_AUTH, REQUEST_AFTER_FAILED_AUTH, RESPONSE, RESPONSE_ASYNC

no

REQUEST_AFTER_AUTH

requiresContent

Only used by Interceptors of proxied resources (the content is always available to Interceptor of Services) Set it to true to make available the content of the request (if interceptPoint is REQUEST_BEFORE_AUTH or REQUEST_AFTER_AUTH) or of the response (if interceptPoint is RESPONSE or RESPONSE_ASYNC)

no

false

priority

the execution priority (less is higher priority)

no

10

Intercept Point Note: REQUEST_BEFORE_EXCHANGE_INIT

An interceptor using this intercept point will handle requests before the exchange initialization occurs. Such interceptors must implement the WildcardInterceptor interface, ensuring that the Interceptor.handle(request, response) method receives both UninitializedRequest and UninitializedResponse objects.

Additionally, interceptors at this stage may:

  • Provide a custom initializer for requests by using PluginUtils.attachCustomRequestInitializer().

  • Modify the raw content of the request using Request.setRawContent().

This setup allows for preliminary modifications and configurations to be applied to requests at an early stage in the processing pipeline.


Transforming Request Content Format

Interceptors at REQUEST_BEFORE_EXCHANGE_INIT can inspect and modify the request before it is initialized. The handle(req, res) and resolve(req, res) methods receive the request as UninitializedRequest and the response as UninitializedResponse. This is particularly useful for transforming request content formats.

Example:

@RegisterPlugin(name = "xmlToBsonTransformer",
    interceptPoint = InterceptPoint.REQUEST_BEFORE_EXCHANGE_INIT,
    description = "transforms XML request to Bson for MongoService")
public class XmlToBsonTransformer implements WildcardInterceptor {
    @Override
    public void handle(UninitializedRequest req, UninitializedResponse res) throws Exception {
        // Transforming XML to Bson
    }

    @Override
    public boolean resolve(UninitializedRequest req, UninitializedResponse res) {
        // Logic to determine when to apply the transformer
    }
}
Important
Only WildcardInterceptor can use the REQUEST_BEFORE_EXCHANGE_INIT intercept point.

For a practical example of transforming request and response content to and from a different format than expected by a service, refer to the protobuffer-contacts example.


Handling Failed Authentication (v9)

RESTHeart v9 introduces the REQUEST_AFTER_FAILED_AUTH intercept point, which executes when authentication or authorization fails. This enables custom handling for failed security attempts before the error response is sent to the client.

Use Cases

  • Brute Force Protection: Detect and respond to repeated failed authentication attempts

  • Custom Error Responses: Provide user-friendly error messages for auth failures

  • Security Event Logging: Track and log authentication failures for audit and security monitoring

  • Rate Limiting: Implement rate limiting based on failed authentication attempts

  • Account Lockout: Temporarily lock accounts after multiple failed attempts

Example: Logging Failed Authentication

@RegisterPlugin(
    name = "failedAuthLogger",
    description = "Logs failed authentication and authorization attempts",
    interceptPoint = InterceptPoint.REQUEST_AFTER_FAILED_AUTH,
    enabledByDefault = true)
public class FailedAuthLoggerInterceptor implements WildcardInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(FailedAuthLoggerInterceptor.class);

    @Override
    public void handle(Request<?> request, Response<?> response) throws Exception {
        var account = request.getAuthenticatedAccount();
        var clientIp = request.getRemoteAddress();
        var path = request.getPath();

        if (account == null) {
            // Authentication failed - no valid credentials provided
            LOGGER.warn("Authentication failed from IP: {} attempting to access: {}",
                clientIp, path);
        } else {
            // Authorization failed - valid credentials but insufficient permissions
            LOGGER.warn("Authorization failed for user: {} from IP: {} attempting to access: {}",
                account.getPrincipal(), clientIp, path);
        }
    }

    @Override
    public boolean resolve(Request<?> request, Response<?> response) {
        return true; // Intercept all failed auth attempts
    }
}

Example: Brute Force Protection

@RegisterPlugin(
    name = "bruteForceProtection",
    description = "Detects and blocks brute force attacks",
    interceptPoint = InterceptPoint.REQUEST_AFTER_FAILED_AUTH,
    enabledByDefault = true)
public class BruteForceProtectionInterceptor implements WildcardInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(BruteForceProtectionInterceptor.class);

    // Track failed attempts per IP
    private final LoadingCache<String, AtomicInteger> failedAttempts = Caffeine.newBuilder()
        .expireAfterWrite(15, TimeUnit.MINUTES)
        .build(key -> new AtomicInteger(0));

    private static final int MAX_ATTEMPTS = 5;
    private static final int LOCKOUT_MINUTES = 15;

    @Override
    public void handle(Request<?> request, Response<?> response) throws Exception {
        var clientIp = request.getRemoteAddress();
        var attempts = failedAttempts.get(clientIp);
        int failedCount = attempts.incrementAndGet();

        if (failedCount >= MAX_ATTEMPTS) {
            LOGGER.error("Brute force attack detected from IP: {}. {} failed attempts. " +
                "Access blocked for {} minutes.",
                clientIp, failedCount, LOCKOUT_MINUTES);

            // Optionally: modify response to indicate account lockout
            response.setStatusCode(429); // Too Many Requests
            response.setContent(BsonDocument.parse(
                "{\"error\": \"Too many failed authentication attempts. " +
                "Account temporarily locked.\"}"
            ));
        } else {
            LOGGER.warn("Failed authentication from IP: {}. Attempt {}/{}",
                clientIp, failedCount, MAX_ATTEMPTS);
        }
    }

    @Override
    public boolean resolve(Request<?> request, Response<?> response) {
        // Only trigger for authentication failures (not authorization)
        return request.getAuthenticatedAccount() == null;
    }
}

Example: Custom Error Response

@RegisterPlugin(
    name = "friendlyAuthErrorResponder",
    description = "Provides user-friendly error messages for auth failures",
    interceptPoint = InterceptPoint.REQUEST_AFTER_FAILED_AUTH,
    enabledByDefault = true)
public class FriendlyAuthErrorInterceptor implements WildcardInterceptor {

    @Override
    public void handle(Request<?> request, Response<?> response) throws Exception {
        var account = request.getAuthenticatedAccount();

        BsonDocument errorResponse;

        if (account == null) {
            // Authentication failed
            errorResponse = BsonDocument.parse("""
                {
                    "error": "Invalid username or password",
                    "code": "AUTH_FAILED",
                    "message": "Please check your credentials and try again."
                }
                """);
        } else {
            // Authorization failed
            errorResponse = BsonDocument.parse("""
                {
                    "error": "Access denied",
                    "code": "INSUFFICIENT_PERMISSIONS",
                    "message": "You do not have permission to access this resource.",
                    "requiredRole": "admin"
                }
                """);
        }

        response.setContent(errorResponse);
    }

    @Override
    public boolean resolve(Request<?> request, Response<?> response) {
        return true; // Apply to all failed auth attempts
    }
}

Key Characteristics

  • Execution Timing: Runs after authentication/authorization fails but before the error response is sent to the client

  • Access to Failure Context: Can distinguish between authentication failures (account == null) and authorization failures (account != null)

  • Response Modification: Can modify the error response status code and body

  • Request Information: Full access to request details (IP address, path, headers, etc.)

Integration with Existing Security

This intercept point works seamlessly with RESTHeart’s existing security infrastructure:

  • Compatible with all authentication mechanisms (Basic Auth, JWT, custom)

  • Works with all authorizers (File ACL, MongoDB ACL, custom)

  • Integrates with the security audit trail

  • No impact on successful authentication/authorization flows

Important
Only WildcardInterceptor can use the REQUEST_AFTER_FAILED_AUTH intercept point, as it needs to handle requests regardless of their specific type.