This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch backport/CAMEL-26281-camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e7d18b4bcaafc6f2e5e15a2b81a24424c5078cb4
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 5 18:06:03 2026 +0100

    CAMEL-26281: camel-whatsapp - verify the X-Hub-Signature-256 signature of 
webhook events by default (#29260)
    
    In webhook mode, the signature of every event is now verified with
    webhookSecret, and a webhook route without webhookSecret fails to start.
    The new webhookVerification option (default true) turns the check off,
    for example in development. An event that reaches the route is answered
    with an empty 200 instead of the request body.
    
    Backport to camel-4.18.x: the parts that do not exist on this branch are
    left out (the body type annotation, the YAML tab in the docs, and the
    security option metadata of webhookVerification with SecurityUtils, which
    camel-4.18.x does not have), and the upgrade guide note stays on main.
    
    (cherry picked from commit 39039a5ce04062c7552a2227b23c6dda70ba6475)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/whatsapp.json  |   3 +-
 .../whatsapp/WhatsAppEndpointConfigurer.java       |   6 +
 .../whatsapp/WhatsAppEndpointUriFactory.java       |   3 +-
 .../apache/camel/component/whatsapp/whatsapp.json  |   3 +-
 .../src/main/docs/whatsapp-component.adoc          |   8 +-
 .../component/whatsapp/WhatsAppConfiguration.java  |  20 ++-
 .../camel/component/whatsapp/WhatsAppEndpoint.java |  11 ++
 .../whatsapp/WhatsAppWebhookProcessor.java         |  24 ++--
 .../whatsapp/WhatsAppWebhookProcessorTest.java     | 139 ++++++++++++++++++++-
 .../whatsapp/WhatsAppWebhookStartupTest.java       |  99 +++++++++++++++
 .../component/whatsapp/WhatsAppWebhookTest.java    |  47 +++++--
 .../dsl/WhatsAppEndpointBuilderFactory.java        |  44 ++++++-
 12 files changed, 377 insertions(+), 30 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json
index e6a569255bb7..3402745d765d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json
@@ -47,6 +47,7 @@
     "webhookVerifyToken": { "index": 6, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "advanced", "label": "advanced", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" },
     "whatsappService": { "index": 7, "kind": "parameter", "displayName": 
"Whatsapp Service", "group": "advanced", "label": "advanced", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.component.whatsapp.WhatsAppService", "deprecated": false, 
"autowired": false, "secret": false, "description": "WhatsApp service 
implementation" },
     "authorizationToken": { "index": 8, "kind": "parameter", "displayName": 
"Authorization Token", "group": "security", "label": "security", "required": 
true, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, 
"configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The authorization access 
token taken from whatsapp-business dashb [...]
-    "webhookSecret": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...]
+    "webhookSecret": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...]
+    "webhookVerification": { "index": 10, "kind": "parameter", "displayName": 
"Webhook Verification", "group": "security", "label": "security", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, 
"configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Whether, in webhook 
mode, the X-Hub-Signature-256 signature of every  [...]
   }
 }
diff --git 
a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java
 
b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java
index d4d2aee025a5..b65bb506880c 100644
--- 
a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java
+++ 
b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java
@@ -37,6 +37,8 @@ public class WhatsAppEndpointConfigurer extends 
PropertyConfigurerSupport implem
         case "webhookPath": 
target.getConfiguration().setWebhookPath(property(camelContext, 
java.lang.String.class, value)); return true;
         case "webhooksecret":
         case "webhookSecret": 
target.getConfiguration().setWebhookSecret(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "webhookverification":
+        case "webhookVerification": 
target.getConfiguration().setWebhookVerification(property(camelContext, 
boolean.class, value)); return true;
         case "webhookverifytoken":
         case "webhookVerifyToken": 
target.getConfiguration().setWebhookVerifyToken(property(camelContext, 
java.lang.String.class, value)); return true;
         case "whatsappservice":
@@ -62,6 +64,8 @@ public class WhatsAppEndpointConfigurer extends 
PropertyConfigurerSupport implem
         case "webhookPath": return java.lang.String.class;
         case "webhooksecret":
         case "webhookSecret": return java.lang.String.class;
+        case "webhookverification":
+        case "webhookVerification": return boolean.class;
         case "webhookverifytoken":
         case "webhookVerifyToken": return java.lang.String.class;
         case "whatsappservice":
@@ -88,6 +92,8 @@ public class WhatsAppEndpointConfigurer extends 
PropertyConfigurerSupport implem
         case "webhookPath": return target.getConfiguration().getWebhookPath();
         case "webhooksecret":
         case "webhookSecret": return 
target.getConfiguration().getWebhookSecret();
+        case "webhookverification":
+        case "webhookVerification": return 
target.getConfiguration().isWebhookVerification();
         case "webhookverifytoken":
         case "webhookVerifyToken": return 
target.getConfiguration().getWebhookVerifyToken();
         case "whatsappservice":
diff --git 
a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java
 
b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java
index 48e36f6d02c5..046b513646ff 100644
--- 
a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java
+++ 
b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java
@@ -23,7 +23,7 @@ public class WhatsAppEndpointUriFactory extends 
org.apache.camel.support.compone
     private static final Set<String> SECRET_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(10);
+        Set<String> props = new HashSet<>(11);
         props.add("apiVersion");
         props.add("authorizationToken");
         props.add("baseUri");
@@ -32,6 +32,7 @@ public class WhatsAppEndpointUriFactory extends 
org.apache.camel.support.compone
         props.add("phoneNumberId");
         props.add("webhookPath");
         props.add("webhookSecret");
+        props.add("webhookVerification");
         props.add("webhookVerifyToken");
         props.add("whatsappService");
         PROPERTY_NAMES = Collections.unmodifiableSet(props);
diff --git 
a/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json
 
b/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json
index e6a569255bb7..3402745d765d 100644
--- 
a/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json
+++ 
b/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json
@@ -47,6 +47,7 @@
     "webhookVerifyToken": { "index": 6, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "advanced", "label": "advanced", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" },
     "whatsappService": { "index": 7, "kind": "parameter", "displayName": 
"Whatsapp Service", "group": "advanced", "label": "advanced", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.component.whatsapp.WhatsAppService", "deprecated": false, 
"autowired": false, "secret": false, "description": "WhatsApp service 
implementation" },
     "authorizationToken": { "index": 8, "kind": "parameter", "displayName": 
"Authorization Token", "group": "security", "label": "security", "required": 
true, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, 
"configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The authorization access 
token taken from whatsapp-business dashb [...]
-    "webhookSecret": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...]
+    "webhookSecret": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...]
+    "webhookVerification": { "index": 10, "kind": "parameter", "displayName": 
"Webhook Verification", "group": "security", "label": "security", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, 
"configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Whether, in webhook 
mode, the X-Hub-Signature-256 signature of every  [...]
   }
 }
diff --git a/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc 
b/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
index 4377ad4f1836..6a49396d2c18 100644
--- a/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
+++ b/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
@@ -102,7 +102,7 @@ In Java DSL:
 
 [source,java]
 ---------------------------------------------------------
-fromF("webhook:whatsapp:%s?authorizationToken=%s&webhookVerifyToken=%s", 
"<phoneNumberId>", "<AuthorizationToken>", 
"<webhookVerifyToken>").log("${body}")
+fromF("webhook:whatsapp:%s?authorizationToken=%s&webhookVerifyToken=%s&webhookSecret=%s",
 "<phoneNumberId>", "<AuthorizationToken>", "<webhookVerifyToken>", 
"<appSecret>").log("${body}")
 ---------------------------------------------------------
 
 You can follow the
@@ -110,4 +110,10 @@ 
https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks[s
 to enable and configure the webhook.
 The webhook component will expose an endpoint that can be used into the 
whatsapp administration console.
 
+WhatsApp signs every event it posts to the webhook with the app secret of the 
Meta app, in the `X-Hub-Signature-256`
+header. Set `webhookSecret` to that app secret (from the Meta app dashboard): 
an event without a valid signature is
+answered with HTTP 403 and does not reach the route, and an event that reaches 
the route is answered with an empty
+HTTP 200. A webhook route without `webhookSecret` fails to start. To accept 
the events without verifying their
+signature, for example in development, set `webhookVerification` to `false`.
+
 Refer to the xref:webhook-component.adoc[camel-webhook component] 
documentation for instructions on how to set it.
diff --git 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java
 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java
index 419ddcd8ba92..3a1e852ade4c 100644
--- 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java
+++ 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java
@@ -47,12 +47,18 @@ public class WhatsAppConfiguration {
     private String webhookPath = "camel-whatsapp/webhook";
 
     @UriParam(description = "The app secret used to verify the 
X-Hub-Signature-256 signature of inbound webhook event"
-                            + " payloads (from the Meta/WhatsApp app 
dashboard). When set, event callbacks with a missing"
-                            + " or invalid signature are rejected with HTTP 
403; when not set, no signature verification"
-                            + " is performed.",
+                            + " payloads (from the Meta/WhatsApp app 
dashboard). An event callback with a missing or invalid"
+                            + " signature is rejected with HTTP 403. Required 
in webhook mode, unless webhookVerification is"
+                            + " false.",
               label = "security", secret = true)
     private String webhookSecret;
 
+    @UriParam(description = "Whether, in webhook mode, the X-Hub-Signature-256 
signature of every event callback is verified"
+                            + " with webhookSecret. When true, the webhook 
route fails to start without webhookSecret. Set it to"
+                            + " false to accept event callbacks without 
verifying their signature, for example in development.",
+              label = "security", defaultValue = "true")
+    private boolean webhookVerification = true;
+
     public WhatsAppConfiguration() {
     }
 
@@ -112,6 +118,14 @@ public class WhatsAppConfiguration {
         this.webhookSecret = webhookSecret;
     }
 
+    public boolean isWebhookVerification() {
+        return webhookVerification;
+    }
+
+    public void setWebhookVerification(boolean webhookVerification) {
+        this.webhookVerification = webhookVerification;
+    }
+
     @Override
     public String toString() {
         return "WhatsAppConfiguration{" + "authorizationToken='" + 
authorizationToken + '\'' + ", baseUri='" + baseUri + '\''
diff --git 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppEndpoint.java
 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppEndpoint.java
index 9902ce8bfabc..963b52a89bd3 100644
--- 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppEndpoint.java
+++ 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppEndpoint.java
@@ -31,6 +31,7 @@ import 
org.apache.camel.component.whatsapp.service.WhatsAppServiceRestAPIAdapter
 import org.apache.camel.spi.UriEndpoint;
 import org.apache.camel.spi.UriParam;
 import org.apache.camel.support.ScheduledPollEndpoint;
+import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,6 +98,16 @@ public class WhatsAppEndpoint extends ScheduledPollEndpoint 
implements WebhookCa
 
     @Override
     public Processor createWebhookHandler(Processor next) {
+        if (!configuration.isWebhookVerification()) {
+            LOG.warn("Webhook signature verification is disabled 
(webhookVerification=false): the events posted to {} are"
+                     + " not verified",
+                    this);
+        } else if (ObjectHelper.isEmpty(configuration.getWebhookSecret())) {
+            throw new IllegalArgumentException(
+                    "webhookSecret must be configured in webhook mode: it is 
the app secret of the Meta app dashboard, used to"
+                                               + " verify the 
X-Hub-Signature-256 signature of the events. Set"
+                                               + " webhookVerification=false 
to accept the events without verifying them");
+        }
         return new WhatsAppWebhookProcessor(next, configuration);
     }
 
diff --git 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
index 0a8bbe0e7e57..9c592e8bc19c 100644
--- 
a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
+++ 
b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java
@@ -35,6 +35,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
 import org.apache.camel.support.AsyncProcessorConverterHelper;
 import org.apache.camel.support.AsyncProcessorSupport;
+import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,8 +59,9 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
     @Override
     public boolean process(Exchange exchange, AsyncCallback callback) {
         String content;
+        boolean event = 
!"GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD, 
String.class));
 
-        if 
("GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD, 
String.class))) {
+        if (!event) {
             // Parse params from the webhook verification request
             Map<String, String> queryParams;
             try {
@@ -98,12 +100,12 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
                 return true;
             }
 
-            // When a webhook secret is configured, verify the 
X-Hub-Signature-256 payload signature
-            String webhookSecret = configuration.getWebhookSecret();
-            if (webhookSecret != null && !isValidSignature(requestBody,
-                    exchange.getIn().getHeader(HUB_SIGNATURE_HEADER, 
String.class), webhookSecret)) {
+            // verify the X-Hub-Signature-256 payload signature, unless 
verification is turned off
+            if (configuration.isWebhookVerification() && 
!isValidSignature(requestBody,
+                    exchange.getIn().getHeader(HUB_SIGNATURE_HEADER, 
String.class), configuration.getWebhookSecret())) {
                 LOG.warn("Rejecting WhatsApp webhook event with missing or 
invalid {} signature", HUB_SIGNATURE_HEADER);
                 exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 
403);
+                exchange.getMessage().setBody(null);
                 callback.done(true);
                 return true;
             }
@@ -114,7 +116,15 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
         exchange.getMessage().setBody(content);
 
         return next.process(exchange, doneSync -> {
-            exchange.getMessage().setBody(content);
+            if (event) {
+                // an event is answered with an empty 200
+                exchange.getMessage().setBody(null);
+                if (!exchange.isFailed() && 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE) == null) {
+                    
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
+                }
+            } else {
+                exchange.getMessage().setBody(content);
+            }
 
             callback.done(doneSync);
         });
@@ -125,7 +135,7 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
      * configured webhook secret, using a constant-time comparison.
      */
     static boolean isValidSignature(byte[] payload, String signatureHeader, 
String secret) {
-        if (signatureHeader == null) {
+        if (signatureHeader == null || ObjectHelper.isEmpty(secret)) {
             return false;
         }
         try {
diff --git 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
index 0ca0c7a1acd9..d9e329512fc9 100644
--- 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
@@ -16,11 +16,18 @@
  */
 package org.apache.camel.component.whatsapp;
 
+import java.nio.charset.StandardCharsets;
+import java.util.HexFormat;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.support.DefaultExchange;
 import org.junit.jupiter.api.AfterEach;
@@ -32,17 +39,25 @@ import static org.assertj.core.api.Assertions.assertThat;
 class WhatsAppWebhookProcessorTest {
 
     private static final String VERIFY_TOKEN = "my token&more";
+    private static final String SECRET = "my-app-secret";
+    private static final String EVENT = 
"{\"object\":\"whatsapp_business_account\"}";
 
     private CamelContext context;
     private WhatsAppWebhookProcessor processor;
     private final AtomicInteger calls = new AtomicInteger();
+    private final AtomicReference<Object> routed = new AtomicReference<>();
+    private final Processor route = exchange -> {
+        calls.incrementAndGet();
+        routed.set(exchange.getMessage().getBody());
+    };
 
     @BeforeEach
     void setUp() {
         context = new DefaultCamelContext();
         WhatsAppConfiguration configuration = new WhatsAppConfiguration();
         configuration.setWebhookVerifyToken(VERIFY_TOKEN);
-        processor = new WhatsAppWebhookProcessor(exchange -> 
calls.incrementAndGet(), configuration);
+        configuration.setWebhookSecret(SECRET);
+        processor = new WhatsAppWebhookProcessor(route, configuration);
         context.start();
     }
 
@@ -126,29 +141,141 @@ class WhatsAppWebhookProcessorTest {
     }
 
     @Test
-    void missingHttpMethodIsAnEvent() {
+    void missingHttpMethodIsAnEvent() throws Exception {
         Exchange exchange = new DefaultExchange(context);
-        exchange.getIn().setBody("{\"object\":\"whatsapp_business_account\"}");
+        exchange.getIn().setBody(EVENT);
+        exchange.getIn().setHeader("X-Hub-Signature-256", sign(EVENT, SECRET));
 
         process(exchange);
 
         assertThat(exchange.getException()).isNull();
-        
assertThat(exchange.getMessage().getBody()).isEqualTo("{\"object\":\"whatsapp_business_account\"}");
+        assertThat(routed).hasValue(EVENT);
         assertThat(calls).hasValue(1);
     }
 
     @Test
-    void eventWithoutBody() {
+    void eventWithoutBody() throws Exception {
         Exchange exchange = new DefaultExchange(context);
         exchange.getIn().setHeader(Exchange.HTTP_METHOD, "POST");
+        exchange.getIn().setHeader("X-Hub-Signature-256", sign("", SECRET));
 
         process(exchange);
 
         assertThat(exchange.getException()).isNull();
-        assertThat(exchange.getMessage().getBody()).isEqualTo("");
+        assertThat(routed).hasValue("");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void eventWithValidSignatureIsRouted() throws Exception {
+        Exchange exchange = event(EVENT, sign(EVENT, SECRET));
+
+        process(exchange);
+
+        assertThat(exchange.getException()).isNull();
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(200);
+        assertThat(routed).hasValue(EVENT);
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void eventWithoutSignatureIsRejectedWith403AndNotRouted() {
+        Exchange exchange = event(EVENT, null);
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+        assertThat(exchange.getMessage().getBody()).isNull();
+        assertThat(calls).hasValue(0);
+    }
+
+    @Test
+    void eventWithSignatureOfAnotherSecretIsRejectedWith403AndNotRouted() 
throws Exception {
+        Exchange exchange = event(EVENT, sign(EVENT, "another-secret"));
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+        assertThat(calls).hasValue(0);
+    }
+
+    @Test
+    void eventIsRejectedWith403WhenTheSecretIsMissing() {
+        // the endpoint refuses to create the handler without a secret; the 
processor does not route either
+        WhatsAppWebhookProcessor withoutSecret = new 
WhatsAppWebhookProcessor(route, new WhatsAppConfiguration());
+        Exchange exchange = event(EVENT, "sha256=");
+
+        withoutSecret.process(exchange, doneSync -> {
+        });
+
+        assertThat(exchange.getException()).isNull();
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+        assertThat(calls).hasValue(0);
+    }
+
+    @Test
+    void eventIsRoutedWithoutSignatureWhenVerificationIsDisabled() throws 
Exception {
+        WhatsAppEndpoint endpoint = context.getEndpoint(
+                
"whatsapp:123?authorizationToken=token&webhookVerification=false", 
WhatsAppEndpoint.class);
+        Processor withoutVerification = endpoint.createWebhookHandler(route);
+        Exchange exchange = event(EVENT, null);
+
+        withoutVerification.process(exchange);
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(routed).hasValue(EVENT);
         assertThat(calls).hasValue(1);
     }
 
+    @Test
+    void eventReplyHasEmptyBody() throws Exception {
+        WhatsAppWebhookProcessor replying = new WhatsAppWebhookProcessor(
+                exchange -> exchange.getMessage().setBody("the result of the 
route"), configurationWithSecret());
+        Exchange exchange = event(EVENT, sign(EVENT, SECRET));
+
+        replying.process(exchange, doneSync -> {
+        });
+
+        assertThat(exchange.getException()).isNull();
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(200);
+        assertThat(exchange.getMessage().getBody()).isNull();
+    }
+
+    @Test
+    void eventReplyKeepsTheResponseCodeOfTheRoute() throws Exception {
+        WhatsAppWebhookProcessor replying = new WhatsAppWebhookProcessor(
+                exchange -> 
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 202), 
configurationWithSecret());
+        Exchange exchange = event(EVENT, sign(EVENT, SECRET));
+
+        replying.process(exchange, doneSync -> {
+        });
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(202);
+        assertThat(exchange.getMessage().getBody()).isNull();
+    }
+
+    private static WhatsAppConfiguration configurationWithSecret() {
+        WhatsAppConfiguration configuration = new WhatsAppConfiguration();
+        configuration.setWebhookSecret(SECRET);
+        return configuration;
+    }
+
+    private Exchange event(String body, String signature) {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setHeader(Exchange.HTTP_METHOD, "POST");
+        exchange.getIn().setBody(body);
+        if (signature != null) {
+            exchange.getIn().setHeader("X-Hub-Signature-256", signature);
+        }
+        return exchange;
+    }
+
+    private static String sign(String payload, String secret) throws Exception 
{
+        Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), 
"HmacSHA256"));
+        return "sha256=" + 
HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
+    }
+
     private Exchange verificationRequest(String header, String query) {
         Exchange exchange = new DefaultExchange(context);
         exchange.getIn().setHeader(Exchange.HTTP_METHOD, "GET");
diff --git 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookStartupTest.java
 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookStartupTest.java
new file mode 100644
index 000000000000..5405769d9ea1
--- /dev/null
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookStartupTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.whatsapp;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class WhatsAppWebhookStartupTest {
+
+    @RegisterExtension
+    AvailablePortFinder.Port port = AvailablePortFinder.find();
+
+    private CamelContext context;
+
+    @BeforeEach
+    void setUp() {
+        context = new DefaultCamelContext();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        context.close();
+    }
+
+    @Test
+    void webhookRouteWithoutSecretFailsToStart() throws Exception {
+        addWebhookRoute("");
+
+        assertThatThrownBy(context::start)
+                .rootCause()
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("webhookSecret")
+                .hasMessageContaining("webhookVerification=false");
+    }
+
+    @Test
+    void webhookRouteWithEmptySecretFailsToStart() throws Exception {
+        addWebhookRoute("&webhookSecret=");
+
+        assertThatThrownBy(context::start)
+                .rootCause()
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("webhookSecret");
+    }
+
+    @Test
+    void webhookRouteWithSecretStarts() throws Exception {
+        addWebhookRoute("&webhookSecret=my-app-secret");
+
+        context.start();
+
+        
assertThat(context.getRouteController().getRouteStatus("webhook")).isEqualTo(ServiceStatus.Started);
+    }
+
+    @Test
+    void webhookRouteWithVerificationDisabledStartsWithoutSecret() throws 
Exception {
+        addWebhookRoute("&webhookVerification=false");
+
+        context.start();
+
+        
assertThat(context.getRouteController().getRouteStatus("webhook")).isEqualTo(ServiceStatus.Started);
+    }
+
+    private void addWebhookRoute(String options) throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                
restConfiguration().component("netty-http").host("localhost").port(port.getPort());
+
+                
from("webhook:whatsapp:123?authorizationToken=token&webhookAutoRegister=false" 
+ options).id("webhook")
+                        .to("mock:endpoint");
+            }
+        });
+    }
+}
diff --git 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
index 6fb4c4575cde..d74be02f579d 100644
--- 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
@@ -17,8 +17,13 @@
 package org.apache.camel.component.whatsapp;
 
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HexFormat;
 import java.util.Map;
 
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
 import org.apache.camel.Exchange;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
@@ -31,6 +36,8 @@ import org.junit.jupiter.api.Test;
 
 public class WhatsAppWebhookTest extends WhatsAppTestSupport {
 
+    private static final String WEBHOOK_SECRET = "my-app-secret";
+
     private int port;
 
     @Test
@@ -88,17 +95,42 @@ public class WhatsAppWebhookTest extends 
WhatsAppTestSupport {
                 = ((WebhookEndpoint) 
context().getRoute("webhook").getConsumer().getEndpoint()).getConfiguration();
         String url = config.computeFullExternalUrl();
 
-        try (InputStream content = 
getClass().getClassLoader().getResourceAsStream("webhook-request.json")) {
-            MockEndpoint mock = getMockEndpoint("mock:endpoint");
-            mock.expectedMinimumMessageCount(1);
-
-            template().sendBodyAndHeader("netty-http:" + url, content, 
Exchange.CONTENT_TYPE, "application/json");
-            mock.assertIsSatisfied();
+        byte[] content;
+        try (InputStream in = 
getClass().getClassLoader().getResourceAsStream("webhook-request.json")) {
+            content = in.readAllBytes();
         }
         MockEndpoint mock = getMockEndpoint("mock:endpoint");
+        mock.expectedMinimumMessageCount(1);
+
+        String reply = template().requestBodyAndHeaders("netty-http:" + url, 
content,
+                Map.of(Exchange.CONTENT_TYPE, "application/json", 
"X-Hub-Signature-256", sign(content)), String.class);
 
         mock.assertIsSatisfied();
         
Assertions.assertThat(mock.getExchanges().get(0).getIn().getBody(String.class)).contains("\"type\":
 \"text\"");
+        Assertions.assertThat(reply).isNullOrEmpty();
+    }
+
+    @Test
+    public void testWebhookEventWithoutSignatureIsRejected() throws Exception {
+        WebhookConfiguration config
+                = ((WebhookEndpoint) 
context().getRoute("webhook").getConsumer().getEndpoint()).getConfiguration();
+        String url = config.computeFullExternalUrl();
+
+        MockEndpoint mock = getMockEndpoint("mock:endpoint");
+        mock.expectedMessageCount(0);
+
+        Assertions
+                .assertThatThrownBy(() -> 
template().sendBodyAndHeader("netty-http:" + url,
+                        "{\"object\":\"whatsapp_business_account\"}", 
Exchange.CONTENT_TYPE, "application/json"))
+                .rootCause()
+                .hasMessageContaining("statusCode: 403");
+        mock.assertIsSatisfied();
+    }
+
+    private static String sign(byte[] payload) throws Exception {
+        Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(new 
SecretKeySpec(WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+        return "sha256=" + HexFormat.of().formatHex(mac.doFinal(payload));
     }
 
     @Override
@@ -117,7 +149,8 @@ public class WhatsAppWebhookTest extends 
WhatsAppTestSupport {
             public void configure() {
                 restConfiguration().host("localhost").port(port);
 
-                from("webhook:whatsapp:" + phoneNumberId + 
"?webhookAutoRegister=false").id("webhook")
+                from("webhook:whatsapp:" + phoneNumberId + 
"?webhookAutoRegister=false&webhookSecret=" + WEBHOOK_SECRET)
+                        .id("webhook")
                         .convertBodyTo(String.class).to("mock:endpoint");
             }
         };
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java
index e658a6112be8..215ef916a6f1 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java
@@ -63,9 +63,9 @@ public interface WhatsAppEndpointBuilderFactory {
         /**
          * The app secret used to verify the X-Hub-Signature-256 signature of
          * inbound webhook event payloads (from the Meta/WhatsApp app
-         * dashboard). When set, event callbacks with a missing or invalid
-         * signature are rejected with HTTP 403; when not set, no signature
-         * verification is performed.
+         * dashboard). An event callback with a missing or invalid signature is
+         * rejected with HTTP 403. Required in webhook mode, unless
+         * webhookVerification is false.
          * 
          * The option is a: <code>java.lang.String</code> type.
          * 
@@ -78,6 +78,44 @@ public interface WhatsAppEndpointBuilderFactory {
             doSetProperty("webhookSecret", webhookSecret);
             return this;
         }
+        /**
+         * Whether, in webhook mode, the X-Hub-Signature-256 signature of every
+         * event callback is verified with webhookSecret. When true, the 
webhook
+         * route fails to start without webhookSecret. Set it to false to 
accept
+         * event callbacks without verifying their signature, for example in
+         * development.
+         * 
+         * The option is a: <code>boolean</code> type.
+         * 
+         * Default: true
+         * Group: security
+         * 
+         * @param webhookVerification the value to set
+         * @return the dsl builder
+         */
+        default WhatsAppEndpointBuilder webhookVerification(boolean 
webhookVerification) {
+            doSetProperty("webhookVerification", webhookVerification);
+            return this;
+        }
+        /**
+         * Whether, in webhook mode, the X-Hub-Signature-256 signature of every
+         * event callback is verified with webhookSecret. When true, the 
webhook
+         * route fails to start without webhookSecret. Set it to false to 
accept
+         * event callbacks without verifying their signature, for example in
+         * development.
+         * 
+         * The option will be converted to a <code>boolean</code> type.
+         * 
+         * Default: true
+         * Group: security
+         * 
+         * @param webhookVerification the value to set
+         * @return the dsl builder
+         */
+        default WhatsAppEndpointBuilder webhookVerification(String 
webhookVerification) {
+            doSetProperty("webhookVerification", webhookVerification);
+            return this;
+        }
     }
 
     /**

Reply via email to