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

gnodet pushed a commit to branch backport/29260-to-camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c3fe83340242f52e7323c2c73f73dbdc94525c75
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Nov 5 17:15:32 2026 +0000

    [backport camel-4.22.x] CAMEL-26281: camel-whatsapp - verify the 
X-Hub-Signature-256 signature of webhook events by default
---
 .../apache/camel/catalog/components/whatsapp.json  |   4 +-
 .../camel/catalog/docs/whatsapp-component.adoc     |   8 +-
 .../whatsapp/WhatsAppEndpointConfigurer.java       |   6 +
 .../whatsapp/WhatsAppEndpointUriFactory.java       |   3 +-
 .../apache/camel/component/whatsapp/whatsapp.json  |   4 +-
 .../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     | 290 +++++++++++++++++++++
 .../whatsapp/WhatsAppWebhookStartupTest.java       |  99 +++++++
 .../component/whatsapp/WhatsAppWebhookTest.java    |  48 +++-
 .../java/org/apache/camel/util/SecurityUtils.java  |   1 +
 .../dsl/WhatsAppEndpointBuilderFactory.java        |  44 +++-
 14 files changed, 545 insertions(+), 25 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 ca98a234bfad..b15c3df41ed2 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,8 @@
     "whatsappService": { "index": 6, "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": 7, "kind": "parameter", "displayName": 
"Authorization Token", "group": "security", "label": "security", "required": 
true, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The authorization access 
token taken from w [...]
     "webhookSecret": { "index": 8, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound  [...]
-    "webhookVerifyToken": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" }
+    "webhookVerification": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Verification", "group": "security", "label": "security", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "security": "insecure:dev", 
"insecureValue": "false", "defaultValue": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Whether, in webh [...]
+    "webhookVerifyToken": { "index": 10, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" }
+
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/whatsapp-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/whatsapp-component.adoc
index 0405c63bf012..ab5cfb1cd062 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/whatsapp-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/whatsapp-component.adoc
@@ -101,7 +101,7 @@ In Java DSL:
 ._Java-only: uses fromF with string formatting_
 [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
@@ -109,4 +109,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/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 a7810bed287e..1554f709fc90 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
@@ -24,7 +24,7 @@ public class WhatsAppEndpointUriFactory extends 
org.apache.camel.support.compone
     private static final Set<String> ENDPOINT_IDENTITY_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");
@@ -33,6 +33,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 ca98a234bfad..b15c3df41ed2 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,8 @@
     "whatsappService": { "index": 6, "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": 7, "kind": "parameter", "displayName": 
"Authorization Token", "group": "security", "label": "security", "required": 
true, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The authorization access 
token taken from w [...]
     "webhookSecret": { "index": 8, "kind": "parameter", "displayName": 
"Webhook Secret", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "The app secret used to 
verify the X-Hub-Signature-256 signature of inbound  [...]
-    "webhookVerifyToken": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" }
+    "webhookVerification": { "index": 9, "kind": "parameter", "displayName": 
"Webhook Verification", "group": "security", "label": "security", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "security": "insecure:dev", 
"insecureValue": "false", "defaultValue": true, "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Whether, in webh [...]
+    "webhookVerifyToken": { "index": 10, "kind": "parameter", "displayName": 
"Webhook Verify Token", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.whatsapp.WhatsAppConfiguration", 
"configurationField": "configuration", "description": "Webhook verify token" }
+
   }
 }
diff --git a/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc 
b/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
index 0405c63bf012..ab5cfb1cd062 100644
--- a/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
+++ b/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
@@ -101,7 +101,7 @@ In Java DSL:
 ._Java-only: uses fromF with string formatting_
 [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
@@ -109,4 +109,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 e252a7e1e592..e9dc888c163f 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", security = "secret")
     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", security = 
"insecure:dev", insecureValue = "false")
+    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 a120824636cf..c32037830fe7 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
@@ -34,6 +34,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;
 
@@ -57,8 +58,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).toString()))
 {
+        if (!event) {
             // Parse params from the webhook verification request
             Map<String, String> queryParams = parseQueryParam(exchange);
 
@@ -89,12 +91,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;
             }
@@ -105,7 +107,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);
         });
@@ -116,7 +126,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
new file mode 100644
index 000000000000..d9e329512fc9
--- /dev/null
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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 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;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+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);
+        configuration.setWebhookSecret(SECRET);
+        processor = new WhatsAppWebhookProcessor(route, configuration);
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        context.close();
+    }
+
+    @Test
+    void emptyQueryValueAndMissingEqualsSign() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY, 
"hub.challenge=&hub.mode=subscribe&flag");
+
+        assertThat(WhatsAppWebhookProcessor.parseQueryParam(exchange))
+                .containsExactlyInAnyOrderEntriesOf(Map.of("hub.challenge", 
"", "hub.mode", "subscribe", "flag", ""));
+
+        process(exchange);
+        // hub.verify_token is missing
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+    }
+
+    @Test
+    void verifiedWithEmptyChallenge() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void encodedTokenAndChallengeAreDecoded() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my+token%26more&hub.challenge=a%3Db%20c");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("a=b c");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void rawQueryIsPreferred() {
+        // netty-http sets the decoded query in CamelHttpQuery and the raw 
query in CamelHttpRawQuery
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                "hub.mode=subscribe&hub.verify_token=my 
token&more&hub.challenge=100%");
+        exchange.getIn().setHeader(Exchange.HTTP_RAW_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=100%25");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("100%");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void wrongTokenIsRejected() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token&hub.challenge=123");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+        assertThat(exchange.getMessage().getBody()).isNull();
+    }
+
+    @Test
+    void invalidEncodingIsBadRequest() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                "hub.mode=subscribe&hub.verify_token=%zz&hub.challenge=123");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+        assertThat(exchange.getException()).isNull();
+        assertThat(calls).hasValue(0);
+    }
+
+    @Test
+    void missingHttpMethodIsAnEvent() throws Exception {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody(EVENT);
+        exchange.getIn().setHeader("X-Hub-Signature-256", sign(EVENT, SECRET));
+
+        process(exchange);
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(routed).hasValue(EVENT);
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    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(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");
+        exchange.getIn().setHeader(header, query);
+        return exchange;
+    }
+
+    private void process(Exchange exchange) {
+        processor.process(exchange, doneSync -> {
+        });
+    }
+}
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 7b34a56d8b36..5eb7501f3967 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,6 +17,12 @@
 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;
@@ -31,6 +37,8 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 
 public class WhatsAppWebhookTest extends WhatsAppTestSupport {
 
+    private static final String WEBHOOK_SECRET = "my-app-secret";
+
     @RegisterExtension
     AvailablePortFinder.Port port = AvailablePortFinder.find();
 
@@ -74,17 +82,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));
     }
 
     protected WhatsAppApiConfig getWhatsAppApiConfig() {
@@ -98,7 +131,8 @@ public class WhatsAppWebhookTest extends WhatsAppTestSupport 
{
             public void configure() {
                 restConfiguration().host("localhost").port(port.getPort());
 
-                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/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java 
b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
index f0846d4e4f9b..33cfc1c638d9 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
@@ -91,6 +91,7 @@ public final class SecurityUtils {
         map.put("validatecertificates", new SecurityOption(INSECURE_SSL, 
VALUE_FALSE));
         map.put("verifyssl", new SecurityOption(INSECURE_SSL, VALUE_FALSE));
         map.put("webhooksecrettokenenabled", new SecurityOption(INSECURE_DEV, 
VALUE_FALSE));
+        map.put("webhookverification", new SecurityOption(INSECURE_DEV, 
VALUE_FALSE));
         map.put("x509hostnameverifier", new SecurityOption(INSECURE_SSL, ""));
         // SECURITY-OPTIONS: END
         SECURITY_OPTIONS = Collections.unmodifiableMap(map);
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 5964e997483f..537f5faa8069 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;
+        }
         /**
          * Webhook verify token.
          * 

Reply via email to