This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 39039a5ce040 CAMEL-26281: camel-whatsapp - verify the
X-Hub-Signature-256 signature of webhook events by default (#29260)
39039a5ce040 is described below
commit 39039a5ce04062c7552a2227b23c6dda70ba6475
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.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/components/whatsapp.json | 9 +-
.../camel/catalog/docs/whatsapp-component.adoc | 9 +-
.../whatsapp/WhatsAppEndpointConfigurer.java | 6 +
.../whatsapp/WhatsAppEndpointUriFactory.java | 3 +-
.../apache/camel/component/whatsapp/whatsapp.json | 9 +-
.../src/main/docs/whatsapp-component.adoc | 9 +-
.../component/whatsapp/WhatsAppConfiguration.java | 20 ++-
.../camel/component/whatsapp/WhatsAppEndpoint.java | 20 ++-
.../whatsapp/WhatsAppWebhookProcessor.java | 24 ++--
.../whatsapp/WhatsAppWebhookProcessorTest.java | 139 ++++++++++++++++++++-
.../whatsapp/WhatsAppWebhookStartupTest.java | 99 +++++++++++++++
.../component/whatsapp/WhatsAppWebhookTest.java | 47 +++++--
.../java/org/apache/camel/util/SecurityUtils.java | 3 +
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 ++
.../dsl/WhatsAppEndpointBuilderFactory.java | 132 +++++++++++++++++--
15 files changed, 494 insertions(+), 47 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 5bd925642ec9..e070baf611e7 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
@@ -39,8 +39,8 @@
"CamelWhatsAppRecipientPhoneNumberId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Recipient phone number associated with
Phone Number ID", "constantName":
"org.apache.camel.component.whatsapp.WhatsAppConstants#WHATSAPP_RECIPIENT_PHONE_NUMBER_ID"
}
},
"bodyTypes": {
- "consumerOut": { "carriers": [ "text", "null" ], "description": "In
webhook mode (webhook:whatsapp:...), the event that WhatsApp posts, as JSON
text. The verification request of WhatsApp (a GET) also reaches the route, with
the hub.challenge as the body when hub.mode is subscribe and hub.verify_token
matches webhookVerifyToken, else with no body (403, or 400 when the parameters
are missing). With webhookSecret, an event without a valid X-Hub-Signature-256
is answered with 403 and doe [...]
- "consumerReply": { "carriers": [ "any" ], "description": "The body at the
end of the route is not sent: the reply is the body the route got, the event or
the hub.challenge" },
+ "consumerOut": { "carriers": [ "text", "null" ], "description": "In
webhook mode (webhook:whatsapp:...), the event that WhatsApp posts, as JSON
text. The verification request of WhatsApp (a GET) also reaches the route, with
the hub.challenge as the body when hub.mode is subscribe and hub.verify_token
matches webhookVerifyToken, else with no body (403, or 400 when the parameters
are missing). An event without a valid X-Hub-Signature-256 is answered with 403
and does not reach the rout [...]
+ "consumerReply": { "carriers": [ "any" ], "description": "The body at the
end of the route is not sent: an event is answered with an empty 200, and the
verification request with the hub.challenge" },
"producerIn": { "carriers": [ "pojo", "null" ], "description": "A request
of org.apache.camel.component.whatsapp.model: a TextMessageRequest,
MediaMessageRequest, LocationMessageRequest, ContactMessageRequest,
InteractiveMessageRequest or TemplateMessageRequest to send a message, or an
UploadMediaRequest to upload a media file. Text, bytes, a Map or another POJO
fails: build the request. No body sends nothing" },
"producerOut": { "carriers": [ "pojo", "unchanged" ], "description": "The
org.apache.camel.component.whatsapp.model.MessageResponse of WhatsApp, with the
ids of the messages sent, or the id of the media uploaded. No body is left as
it is" }
},
@@ -56,7 +56,8 @@
"webhookPath": { "index": 8, "kind": "parameter", "displayName": "Webhook
Path", "group": "advanced", "label": "advanced", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "webhook", "configurationClass":
"org.apache.camel.component.whatsapp.WhatsAppConfiguration",
"configurationField": "configuration", "description": "Webhook path" },
"whatsappService": { "index": 9, "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": 10, "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 [...]
- "webhookSecret": { "index": 11, "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": 12, "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" }
+ "webhookSecret": { "index": 11, "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 [...]
+ "webhookVerification": { "index": 12, "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 web [...]
+ "webhookVerifyToken": { "index": 13, "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 1da15a25c2c2..52ce85444e87 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
@@ -126,7 +126,7 @@ Java::
+
[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}")
----
YAML::
@@ -139,6 +139,7 @@ YAML::
parameters:
authorizationToken: "{{whatsapp.authorization-token}}"
webhookVerifyToken: "{{whatsapp.webhook-verify-token}}"
+ webhookSecret: "{{whatsapp.app-secret}}"
steps:
- log:
message: "${body}"
@@ -150,4 +151,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 a1e10c93d474..8b33e466b472 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
@@ -44,6 +44,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":
@@ -75,6 +77,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":
@@ -107,6 +111,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 63ab2f0d1ad4..ef1631d8796e 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<>(13);
+ Set<String> props = new HashSet<>(14);
props.add("apiVersion");
props.add("authorizationToken");
props.add("baseUri");
@@ -36,6 +36,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 5bd925642ec9..e070baf611e7 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
@@ -39,8 +39,8 @@
"CamelWhatsAppRecipientPhoneNumberId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Recipient phone number associated with
Phone Number ID", "constantName":
"org.apache.camel.component.whatsapp.WhatsAppConstants#WHATSAPP_RECIPIENT_PHONE_NUMBER_ID"
}
},
"bodyTypes": {
- "consumerOut": { "carriers": [ "text", "null" ], "description": "In
webhook mode (webhook:whatsapp:...), the event that WhatsApp posts, as JSON
text. The verification request of WhatsApp (a GET) also reaches the route, with
the hub.challenge as the body when hub.mode is subscribe and hub.verify_token
matches webhookVerifyToken, else with no body (403, or 400 when the parameters
are missing). With webhookSecret, an event without a valid X-Hub-Signature-256
is answered with 403 and doe [...]
- "consumerReply": { "carriers": [ "any" ], "description": "The body at the
end of the route is not sent: the reply is the body the route got, the event or
the hub.challenge" },
+ "consumerOut": { "carriers": [ "text", "null" ], "description": "In
webhook mode (webhook:whatsapp:...), the event that WhatsApp posts, as JSON
text. The verification request of WhatsApp (a GET) also reaches the route, with
the hub.challenge as the body when hub.mode is subscribe and hub.verify_token
matches webhookVerifyToken, else with no body (403, or 400 when the parameters
are missing). An event without a valid X-Hub-Signature-256 is answered with 403
and does not reach the rout [...]
+ "consumerReply": { "carriers": [ "any" ], "description": "The body at the
end of the route is not sent: an event is answered with an empty 200, and the
verification request with the hub.challenge" },
"producerIn": { "carriers": [ "pojo", "null" ], "description": "A request
of org.apache.camel.component.whatsapp.model: a TextMessageRequest,
MediaMessageRequest, LocationMessageRequest, ContactMessageRequest,
InteractiveMessageRequest or TemplateMessageRequest to send a message, or an
UploadMediaRequest to upload a media file. Text, bytes, a Map or another POJO
fails: build the request. No body sends nothing" },
"producerOut": { "carriers": [ "pojo", "unchanged" ], "description": "The
org.apache.camel.component.whatsapp.model.MessageResponse of WhatsApp, with the
ids of the messages sent, or the id of the media uploaded. No body is left as
it is" }
},
@@ -56,7 +56,8 @@
"webhookPath": { "index": 8, "kind": "parameter", "displayName": "Webhook
Path", "group": "advanced", "label": "advanced", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false, "autowired":
false, "secret": false, "defaultValue": "webhook", "configurationClass":
"org.apache.camel.component.whatsapp.WhatsAppConfiguration",
"configurationField": "configuration", "description": "Webhook path" },
"whatsappService": { "index": 9, "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": 10, "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 [...]
- "webhookSecret": { "index": 11, "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": 12, "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" }
+ "webhookSecret": { "index": 11, "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 [...]
+ "webhookVerification": { "index": 12, "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 web [...]
+ "webhookVerifyToken": { "index": 13, "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 1da15a25c2c2..52ce85444e87 100644
--- a/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
+++ b/components/camel-whatsapp/src/main/docs/whatsapp-component.adoc
@@ -126,7 +126,7 @@ Java::
+
[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}")
----
YAML::
@@ -139,6 +139,7 @@ YAML::
parameters:
authorizationToken: "{{whatsapp.authorization-token}}"
webhookVerifyToken: "{{whatsapp.webhook-verify-token}}"
+ webhookSecret: "{{whatsapp.app-secret}}"
steps:
- log:
message: "${body}"
@@ -150,4 +151,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 85b84efdcb20..cf4ac5a4ac79 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
@@ -34,6 +34,7 @@ import org.apache.camel.spi.BodyTypes;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,11 +51,12 @@ import org.slf4j.LoggerFactory;
+ " text. The verification
request of WhatsApp (a GET) also reaches the route,"
+ " with the hub.challenge as
the body when hub.mode is subscribe and"
+ " hub.verify_token matches
webhookVerifyToken, else with no body (403, or 400"
- + " when the parameters are
missing). With webhookSecret, an event without a"
- + " valid X-Hub-Signature-256
is answered with 403 and does not reach the route"),
+ + " when the parameters are
missing). An event without a valid"
+ + " X-Hub-Signature-256 is
answered with 403 and does not reach the route, unless"
+ + " webhookVerification is
false"),
consumerReply = @BodyType(value = Carrier.ANY,
- description = "The body at the end of the
route is not sent: the reply is the body the route"
- + " got, the event or the
hub.challenge"),
+ description = "The body at the end of the
route is not sent: an event is answered with an"
+ + " empty 200, and the
verification request with the hub.challenge"),
producerIn = @BodyType(value = { Carrier.POJO, Carrier.NULL },
description = "A request of
org.apache.camel.component.whatsapp.model: a TextMessageRequest,"
+ " MediaMessageRequest,
LocationMessageRequest, ContactMessageRequest,"
@@ -118,6 +120,16 @@ public class WhatsAppEndpoint extends DefaultEndpoint
implements WebhookCapableE
@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 564df7cb7668..fbbf372ddfe9 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;
@@ -32,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();
@@ -90,17 +97,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() {
@@ -114,7 +146,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 9a7e15b986c4..80abc1dfe62c 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
@@ -109,6 +109,7 @@ public final class SecurityUtils {
map.put("validateauth", new SecurityOption(INSECURE_DEV, 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);
@@ -336,6 +337,8 @@ public final class SecurityUtils {
"component:ibmwatsonxai"));
owners.put("webhooksecrettokenenabled", Set.of(
"component:telegram"));
+ owners.put("webhookverification", Set.of(
+ "component:whatsapp"));
owners.put("x509hostnameverifier", Set.of(
"component:http",
"component:https"));
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 4eac5562bfe3..34e125ebd9fe 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2759,6 +2759,18 @@ registered.
To restore the previous behaviour, set `webhookSecretTokenEnabled=false`: the
webhook is then registered without a
secret token, and webhook requests are not verified.
+=== camel-whatsapp - webhook events are verified by default
+
+In webhook mode (`webhook:whatsapp:...`), the `X-Hub-Signature-256` signature
of every event that WhatsApp posts is
+now verified with the `webhookSecret` option, the app secret from the Meta app
dashboard. A webhook route without
+`webhookSecret` now fails to start. Set `webhookSecret` to the app secret, or
set the new `webhookVerification` option
+to `false` to accept the events without verifying their signature, for example
in development. An event without a
+valid signature is answered with HTTP 403 and does not reach the route. A
client that posts events to the webhook
+endpoint directly, for example in a test, must sign them too.
+
+An event that reaches the route is now answered with HTTP 200 and an empty
body. Before, the reply carried the event
+back to the caller. The reply to the verification request (`GET`) is still the
`hub.challenge`.
+
=== camel-paho-mqtt5 - a failed message with manualAcksEnabled is delivered
again or acknowledged
With `manualAcksEnabled=true`, a message whose exchange failed was left
unacknowledged. For a QoS 1 message the
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 4f211a19d822..680b7013a0bf 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
@@ -62,9 +62,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.
*
@@ -77,6 +77,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 WhatsAppEndpointConsumerBuilder 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 WhatsAppEndpointConsumerBuilder webhookVerification(String
webhookVerification) {
+ doSetProperty("webhookVerification", webhookVerification);
+ return this;
+ }
/**
* Webhook verify token.
*
@@ -355,9 +393,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.
*
@@ -370,6 +408,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 WhatsAppEndpointProducerBuilder 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 WhatsAppEndpointProducerBuilder webhookVerification(String
webhookVerification) {
+ doSetProperty("webhookVerification", webhookVerification);
+ return this;
+ }
/**
* Webhook verify token.
*
@@ -577,9 +653,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.
*
@@ -592,6 +668,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.
*