webhookAuthHeaders = resolveWebhookAuthHeaders(props.getWebhookAuth());
+
String baseUrl = props.getBaseUrl();
if (baseUrl != null && !baseUrl.isBlank()) {
- return new N8nWebhookService(
- props.resolvedBaseUrl(), props.getApiKey(), Collections.emptyMap(), n8nRestClient);
+ return new N8nWebhookService(props.resolvedBaseUrl(), webhookAuthHeaders, n8nRestClient);
}
// base-url is missing — behaviour depends on active profile
if (environment.matchesProfiles("development")) {
@@ -270,8 +382,7 @@ public N8nWebhookService n8nWebhookService(
N8nProperties devProps = new N8nProperties();
devProps.setBaseUrl("http://localhost:5678");
devProps.setUseTestWebhook(props.isUseTestWebhook());
- return new N8nWebhookService(
- devProps.resolvedBaseUrl(), props.getApiKey(), Collections.emptyMap(), n8nRestClient);
+ return new N8nWebhookService(devProps.resolvedBaseUrl(), webhookAuthHeaders, n8nRestClient);
}
// non-dev profile: fail fast at startup so misconfiguration is caught immediately
throw new IllegalStateException(
diff --git a/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/ConsoleN8NWebhookService.java b/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/ConsoleN8NWebhookService.java
index c334882..0dc5063 100644
--- a/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/ConsoleN8NWebhookService.java
+++ b/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/ConsoleN8NWebhookService.java
@@ -22,7 +22,7 @@ public class ConsoleN8NWebhookService extends N8nWebhookService {
private final AtomicInteger counter = new AtomicInteger(0);
public ConsoleN8NWebhookService() {
- super("http://console", "", Collections.emptyMap(), null);
+ super("http://console", Collections.emptyMap(), null);
}
/** Instead of making an HTTP call, logs the path (and payload at DEBUG level). */
diff --git a/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/N8nWebhookService.java b/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/N8nWebhookService.java
index 5beecb3..7b4fae3 100644
--- a/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/N8nWebhookService.java
+++ b/cds-feature-n8n/src/main/java/com/sap/cds/feature/n8n/services/N8nWebhookService.java
@@ -16,8 +16,8 @@
* HTTP layer for calling n8n webhooks.
*
* Sends a {@code POST} to {@code baseUrl/path} with the payload as JSON. Auth headers are
- * layered in order: {@code authHeaders} first (e.g. {@code Authorization: Bearer …} from a BTP
- * destination), then {@code X-N8N-API-KEY}. Constructed by {@link
+ * injected from {@code authHeaders} (e.g. {@code Authorization: Bearer …} from a BTP destination,
+ * or resolved from the {@code n8n.webhook-auth} configuration). Constructed by {@link
* com.sap.cds.feature.n8n.configuration.N8nAutoConfiguration} with pre-configured timeouts.
*/
public class N8nWebhookService {
@@ -25,21 +25,17 @@ public class N8nWebhookService {
private static final Logger log = LoggerFactory.getLogger(N8nWebhookService.class);
private final String baseUrl;
- private final String apiKey;
private final Map authHeaders;
private final RestClient restClient;
/**
* @param baseUrl n8n webhook base URL; trailing slash is stripped automatically
- * @param apiKey sent as {@code X-N8N-API-KEY}; may be empty
- * @param authHeaders proxy auth headers (e.g. {@code Authorization: Bearer …} from a BTP
- * destination); may be empty
+ * @param authHeaders auth headers for the webhook node (e.g. {@code Authorization: Bearer …} from
+ * a BTP destination or from {@code n8n.webhook-auth} config); may be empty
* @param restClient pre-configured {@link RestClient} with connect/read timeouts
*/
- public N8nWebhookService(
- String baseUrl, String apiKey, Map authHeaders, RestClient restClient) {
+ public N8nWebhookService(String baseUrl, Map authHeaders, RestClient restClient) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
- this.apiKey = apiKey != null ? apiKey : "";
this.authHeaders = authHeaders != null ? authHeaders : Collections.emptyMap();
this.restClient = restClient;
}
@@ -65,11 +61,7 @@ public void notify(String path, Map payload, HttpMethod httpMeth
restClient
.method(httpMethod)
.uri(useQueryParams ? buildUriWithParams(path, payload) : baseUrl + "/" + path)
- .headers(
- header -> {
- authHeaders.forEach(header::set);
- if (!apiKey.isEmpty()) header.set("X-N8N-API-KEY", apiKey);
- });
+ .headers(header -> authHeaders.forEach(header::set));
if (useQueryParams) {
spec.retrieve().toBodilessEntity();
} else {
diff --git a/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/configuration/N8nAutoConfigurationTest.java b/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/configuration/N8nAutoConfigurationTest.java
index 44d807d..c93f6e5 100644
--- a/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/configuration/N8nAutoConfigurationTest.java
+++ b/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/configuration/N8nAutoConfigurationTest.java
@@ -12,6 +12,7 @@
import com.sap.cds.feature.n8n.configuration.N8nAutoConfiguration.DestinationConfiguration;
import com.sap.cds.feature.n8n.configuration.N8nAutoConfiguration.N8nProperties;
+import com.sap.cds.feature.n8n.configuration.N8nAutoConfiguration.N8nProperties.WebhookAuth;
import com.sap.cds.feature.n8n.handlers.N8nHandler;
import com.sap.cds.feature.n8n.handlers.N8nServiceHandler;
import com.sap.cds.feature.n8n.services.ConsoleN8NWebhookService;
@@ -23,6 +24,8 @@
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import java.lang.reflect.Field;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -158,6 +161,104 @@ void resolvedBaseUrl_trailingSlashStripped() {
assertThat(props.resolvedBaseUrl()).isEqualTo("http://n8n.example.com/webhook");
}
+ // --- webhookAuth header resolution ---
+
+ @Test
+ void webhookAuth_null_returnsEmptyMap() {
+ assertThat(N8nAutoConfiguration.resolveWebhookAuthHeaders(null)).isEmpty();
+ }
+
+ @Test
+ void webhookAuth_noType_returnsEmptyMap() {
+ assertThat(N8nAutoConfiguration.resolveWebhookAuthHeaders(new WebhookAuth())).isEmpty();
+ }
+
+ @Test
+ void webhookAuth_basic_encodesCredentials() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("basic");
+ auth.setUsername("user");
+ auth.setPassword("pass");
+ String expected =
+ Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8));
+ assertThat(N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .containsEntry("Authorization", "Basic " + expected);
+ }
+
+ @Test
+ void webhookAuth_basic_missingPassword_throws() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("basic");
+ auth.setUsername("user");
+ assertThatThrownBy(() -> N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("basic");
+ }
+
+ @Test
+ void webhookAuth_header_setsCustomHeader() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("header");
+ auth.setName("X-Webhook-Secret");
+ auth.setValue("my-secret");
+ assertThat(N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .containsEntry("X-Webhook-Secret", "my-secret");
+ }
+
+ @Test
+ void webhookAuth_header_missingValue_throws() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("header");
+ auth.setName("X-My-Header");
+ assertThatThrownBy(() -> N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("header");
+ }
+
+ @Test
+ void webhookAuth_bearer_setsBearerToken() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("bearer");
+ auth.setToken("my-token");
+ assertThat(N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .containsEntry("Authorization", "Bearer my-token");
+ }
+
+ @Test
+ void webhookAuth_bearer_missingToken_throws() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("bearer");
+ assertThatThrownBy(() -> N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("bearer");
+ }
+
+ @Test
+ void webhookAuth_unsupportedType_throws() {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("digest");
+ assertThatThrownBy(() -> N8nAutoConfiguration.resolveWebhookAuthHeaders(auth))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("digest");
+ }
+
+ @Test
+ void webhookAuth_basic_appearsInWebhookServiceAuthHeaders() throws Exception {
+ WebhookAuth auth = new WebhookAuth();
+ auth.setType("basic");
+ auth.setUsername("user");
+ auth.setPassword("pass");
+ N8nProperties props = propsWithBaseUrl("http://n8n.example.com");
+ props.setWebhookAuth(auth);
+
+ N8nWebhookService bean = config.n8nWebhookService(props, mock(RestClient.class), mockEnv());
+
+ String expected =
+ Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8));
+ assertThat((Map) field(bean, "authHeaders"))
+ .containsEntry("Authorization", "Basic " + expected);
+ }
+
// --- BTP destination ---
@Test
@@ -179,14 +280,14 @@ void destination_set_resolves_baseUrlAndAuthHeadersFromDestination() throws Exce
propsWithDestination("my-dest"), mock(RestClient.class));
assertThat((String) field(bean, "baseUrl")).isEqualTo("https://n8n.example.com/webhook");
- assertThat((String) field(bean, "apiKey")).isEmpty();
assertThat((Map) field(bean, "authHeaders"))
.containsEntry("Authorization", "Bearer test-token");
}
}
@Test
- void destination_set_apiKeyOverride_takesPreference() throws Exception {
+ void destination_xN8nApiKeyHeader_isNotForwardedToWebhooks() throws Exception {
+ // X-N8N-API-KEY from the destination is a REST API credential — must not reach webhook nodes
HttpDestination mockDest = mock(HttpDestination.class);
Destination mockDestWrapper = mock(Destination.class);
when(mockDest.getUri()).thenReturn(URI.create("https://n8n.example.com"));
@@ -202,14 +303,10 @@ void destination_set_apiKeyOverride_takesPreference() throws Exception {
.when(() -> DestinationAccessor.getDestination("my-dest"))
.thenReturn(mockDestWrapper);
- N8nProperties props = propsWithDestination("my-dest");
- props.setApiKey("explicit-override");
-
N8nWebhookService bean =
- destConfig.n8nWebhookServiceFromDestination(props, mock(RestClient.class));
+ destConfig.n8nWebhookServiceFromDestination(
+ propsWithDestination("my-dest"), mock(RestClient.class));
- assertThat((String) field(bean, "apiKey")).isEqualTo("explicit-override");
- // X-N8N-API-KEY from the destination must not leak into authHeaders
assertThat((Map) field(bean, "authHeaders"))
.doesNotContainKey("X-N8N-API-KEY")
.containsEntry("Authorization", "Bearer test-token");
diff --git a/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/services/N8nWebhookServiceRetryIT.java b/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/services/N8nWebhookServiceRetryIT.java
index 07690f3..5c280cc 100644
--- a/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/services/N8nWebhookServiceRetryIT.java
+++ b/cds-feature-n8n/src/test/java/com/sap/cds/feature/n8n/services/N8nWebhookServiceRetryIT.java
@@ -50,10 +50,7 @@ N8nWebhookService n8nWebhookService() {
factory.setReadTimeout(500);
RestClient restClient = RestClient.builder().requestFactory(factory).build();
return new N8nWebhookService(
- "http://localhost:" + wireMock.port(),
- "test-api-key",
- java.util.Collections.emptyMap(),
- restClient);
+ "http://localhost:" + wireMock.port(), java.util.Collections.emptyMap(), restClient);
}
}
diff --git a/docs/adr-webhook-authentication.md b/docs/adr-webhook-authentication.md
index 647cc09..e4a3918 100644
--- a/docs/adr-webhook-authentication.md
+++ b/docs/adr-webhook-authentication.md
@@ -5,7 +5,7 @@
| . | . |
|--------------|----------------------|
| Date | 2026-08-13 |
-| Version | V0.1 |
+| Version | V0.2 |
| Status | Draft |
| Acceptance | Accepted |
| Contributors | Lisa Nebel |
@@ -13,13 +13,16 @@
**Version History**
-| Version | Date | Changes |
-|---------|------------|-----------------|
-| V0.1 | 2026-08-13 | Initial version |
+| Version | Date | Changes |
+|---------|------------|---------------------------------------------------------------------------------|
+| V0.1 | 2026-08-13 | Initial version |
+| V0.2 | 2026-09-24 | Revised decision: replace fixed `X-N8N-API-KEY` with configurable webhook auth |
## Summary
-The cds-feature-n8n plugin needs to authenticate outbound webhook calls from CAP to n8n. Two approaches were considered: a single shared header (`X-N8N-API-KEY`) sent on all requests, or configurable per-trigger headers. The single shared header approach was chosen for simplicity, with the architecture intentionally designed to allow per-trigger headers to be added later without breaking changes.
+The cds-feature-n8n plugin needs to authenticate outbound webhook calls from CAP to n8n. Two approaches were considered: a single shared header (`X-N8N-API-KEY`) sent on all requests, or configurable per-trigger headers. The single shared header approach was chosen initially (V0.1) for simplicity.
+
+**V0.2 revision:** `X-N8N-API-KEY` is an n8n REST API credential — forwarding it to webhook nodes is semantically incorrect and unnecessarily couples webhook auth to the REST API key. Webhook authentication is now opt-in and configurable via `n8n.webhook-auth.*` properties, matching the three auth types that n8n's Webhook node supports natively.
## Context
@@ -50,6 +53,7 @@ One API key is configured globally. The plugin sends `X-N8N-API-KEY: ` on e
- ❌ All webhooks share the same secret — no per-webhook isolation
- ❌ Header name is hardcoded — if a workflow author uses a different header name on their Webhook node, the plugin cannot reach it without code changes
- ❌ Does not naturally extend to the BTP proxy case without additional `authHeaders` layering
+- ❌ `X-N8N-API-KEY` is a REST API credential — sending it to webhook nodes is semantically wrong
**Option 2: Configurable per-trigger headers**
@@ -68,12 +72,47 @@ The `@n8n.process.start` annotation accepts an optional `headers` map per trigge
- ❌ More complex annotation syntax and handler implementation
- ❌ Secrets in CDS annotations are not ideal — would need env var interpolation support
-## Decision
+**Option 3: Configurable global webhook auth (chosen in V0.2)**
+
+Webhook authentication is configured once via `n8n.webhook-auth.*` properties, independent of `n8n.api-key`. Three types are supported, matching n8n's Webhook node auth options:
+
+```yaml
+n8n:
+ webhook-auth:
+ type: header # basic | header | bearer
+ name: X-My-Header # for type=header
+ value: ${N8N_WEBHOOK_TOKEN}
+```
+
+- ✅ Decouples webhook auth from the REST API key
+- ✅ Supports all three auth types the n8n Webhook node offers (Basic Auth, Header Auth, Bearer)
+- ✅ No secrets in CDS annotations — credentials stay in application config / env vars
+- ✅ Aligns with the Node.js plugin pattern
+- ✅ Naturally extends to BTP: destination headers are forwarded first, `webhook-auth` merged on top
+- ❌ Single global config — all webhook triggers share the same auth (per-trigger auth remains a future extension)
+
+## Decision (V0.2 — Current)
+
+Option 3. `X-N8N-API-KEY` is no longer forwarded to webhook nodes. Webhook authentication is opt-in and configured via `n8n.webhook-auth.*`:
+
+| `type` | Required fields | Header sent |
+|----------|------------------------|------------------------------------------------|
+| `basic` | `username`, `password` | `Authorization: Basic base64(username:password)` |
+| `header` | `name`, `value` | `: ` |
+| `bearer` | `token` | `Authorization: Bearer ` |
+
+When `n8n.webhook-auth` is not set, webhook calls are sent without any authentication header — suitable for n8n instances where the Webhook node has no auth configured.
+
+`n8n.api-key` is retained in the configuration model for future REST API calls (`/api/v1/…`) but is no longer forwarded to webhook nodes.
+
+For the BTP destination path, destination headers are forwarded as before (excluding `X-N8N-API-KEY`), with `n8n.webhook-auth` merged on top (overriding any destination header of the same name).
+
+## Decision (V0.1 — Superseded)
-Option 1 (single shared header) was chosen as the default behaviour for now, since n8n's Webhook node does not mandate a header name, workflow authors pick their own. **Aligning on `X-N8N-API-KEY` keeps the setup story simple: one API key, one header, everywhere.**
+Option 1 (single shared header) was chosen as the default behaviour, since n8n's Webhook node does not mandate a header name, workflow authors pick their own. Aligning on `X-N8N-API-KEY` keeps the setup story simple: one API key, one header, everywhere.
-This matches the Node plugin at https://github.com/cap-js/n8n/.
-For the BTP proxy case, the destination-based configuration already handles the two-layer auth problem: `authHeaders` from the BTP destination (e.g. `Authorization: Bearer ...`) are merged onto the request first, then `X-N8N-API-KEY` is added on top.
+This matched the Node.js plugin at https://github.com/cap-js/n8n/.
+For the BTP proxy case, the destination-based configuration already handled the two-layer auth problem: `authHeaders` from the BTP destination (e.g. `Authorization: Bearer ...`) were merged onto the request first, then `X-N8N-API-KEY` was added on top.
## Future Extension
@@ -81,7 +120,7 @@ Per-trigger configurable headers (Option 2) are explicitly kept as a future exte
## Related
-- `N8nWebhookService.java` — already accepts `Map authHeaders` layered before `X-N8N-API-KEY`
-- `N8nAutoConfiguration.java` — destination resolution populates `authHeaders` from BTP destination headers
+- `N8nWebhookService.java` — accepts `Map authHeaders`; no longer adds `X-N8N-API-KEY`
+- `N8nAutoConfiguration.java` — `resolveWebhookAuthHeaders()` converts `n8n.webhook-auth` config to headers; destination resolution populates `authHeaders` from BTP destination headers (excluding `X-N8N-API-KEY`)
- Node.js plugin `lib/api/connection.js` — `buildWebhookHeaders()` / `buildApiHeaders()` / `authHeaders` from destination
- n8n REST API docs — `X-N8N-API-KEY` is the required header for `/api/v1/…` endpoints
diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/srv/src/main/resources/application.yaml
index d84c4f5..c55a2aa 100644
--- a/integration-tests/srv/src/main/resources/application.yaml
+++ b/integration-tests/srv/src/main/resources/application.yaml
@@ -20,4 +20,3 @@ cds:
ordered: true
n8n:
base-url: http://localhost:${wiremock.port}
- api-key: test-key
diff --git a/integration-tests/srv/src/test/java/integrationtest/N8nAssociationInputIntegrationTest.java b/integration-tests/srv/src/test/java/integrationtest/N8nAssociationInputIntegrationTest.java
index ff1a34a..8d9a2f5 100644
--- a/integration-tests/srv/src/test/java/integrationtest/N8nAssociationInputIntegrationTest.java
+++ b/integration-tests/srv/src/test/java/integrationtest/N8nAssociationInputIntegrationTest.java
@@ -54,7 +54,6 @@ static void stopWireMock() {
@DynamicPropertySource
static void n8nBaseUrl(DynamicPropertyRegistry registry) {
registry.add("n8n.base-url", () -> "http://localhost:" + wireMock.port());
- registry.add("n8n.api-key", () -> "test-key");
}
@BeforeEach
diff --git a/integration-tests/srv/src/test/java/integrationtest/N8nIfConditionIntegrationTest.java b/integration-tests/srv/src/test/java/integrationtest/N8nIfConditionIntegrationTest.java
index a6339a6..4f6a767 100644
--- a/integration-tests/srv/src/test/java/integrationtest/N8nIfConditionIntegrationTest.java
+++ b/integration-tests/srv/src/test/java/integrationtest/N8nIfConditionIntegrationTest.java
@@ -4,7 +4,6 @@
package integrationtest;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
@@ -53,7 +52,6 @@ static void stopWireMock() {
@DynamicPropertySource
static void n8nBaseUrl(DynamicPropertyRegistry registry) {
registry.add("n8n.base-url", () -> "http://localhost:" + wireMock.port());
- registry.add("n8n.api-key", () -> "test-key");
}
@BeforeEach
@@ -92,7 +90,6 @@ void createItem_ifConditionMet_firesConditionalWebhook() {
wireMock.verify(
1,
postRequestedFor(urlEqualTo("/webhook/item-shipped"))
- .withHeader("X-N8N-API-KEY", equalTo("test-key"))
.withRequestBody(
equalToJson(
"{\"ID\":\"" + id + "\",\"status\":\"shipped\"}", true, false))));
@@ -247,7 +244,6 @@ void deleteItem_ifConditionMet_firesConditionalWebhook() {
wireMock.verify(
1,
postRequestedFor(urlEqualTo("/webhook/item-active-deleted"))
- .withHeader("X-N8N-API-KEY", equalTo("test-key"))
.withRequestBody(
equalToJson(
"{\"ID\":\"" + id + "\",\"status\":\"active\"}", true, false))));
diff --git a/integration-tests/srv/src/test/java/integrationtest/N8nIntegrationTest.java b/integration-tests/srv/src/test/java/integrationtest/N8nIntegrationTest.java
index 94798ef..6c0f463 100644
--- a/integration-tests/srv/src/test/java/integrationtest/N8nIntegrationTest.java
+++ b/integration-tests/srv/src/test/java/integrationtest/N8nIntegrationTest.java
@@ -4,7 +4,6 @@
package integrationtest;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
@@ -54,7 +53,6 @@ static void stopWireMock() {
@DynamicPropertySource
static void n8nBaseUrl(DynamicPropertyRegistry registry) {
registry.add("n8n.base-url", () -> "http://localhost:" + wireMock.port());
- registry.add("n8n.api-key", () -> "test-key");
}
@BeforeEach
@@ -88,7 +86,6 @@ void createItem_triggersN8nWebhook_withCorrectPayload() {
wireMock.verify(
1,
postRequestedFor(urlEqualTo("/webhook/item-created"))
- .withHeader("X-N8N-API-KEY", equalTo("test-key"))
.withRequestBody(
equalToJson(
"{\"ID\":\"" + id + "\",\"title\":\"Test Item\"}", true, false))));
@@ -118,7 +115,6 @@ void deleteItem_triggersN8nWebhook_withCorrectPayload() {
wireMock.verify(
1,
postRequestedFor(urlEqualTo("/webhook/item-deleted"))
- .withHeader("X-N8N-API-KEY", equalTo("test-key"))
.withRequestBody(
equalToJson(
"{\"ID\":\""
@@ -172,7 +168,6 @@ void createOrder_noInputsAnnotation_sendsAllScalarFields() {
wireMock.verify(
1,
postRequestedFor(urlEqualTo("/webhook/order-created"))
- .withHeader("X-N8N-API-KEY", equalTo("test-key"))
.withRequestBody(
equalToJson("{\"ID\":\"" + id + "\",\"total\":42}", true, false))));
}
diff --git a/samples/bookshop/srv/src/main/resources/application.yaml b/samples/bookshop/srv/src/main/resources/application.yaml
index c4f33c1..dac2b3c 100644
--- a/samples/bookshop/srv/src/main/resources/application.yaml
+++ b/samples/bookshop/srv/src/main/resources/application.yaml
@@ -30,6 +30,12 @@ cds:
n8n:
# n8n host only — no /webhook suffix. The plugin appends /webhook or /webhook-test automatically.
base-url: ${N8N_BASE_URL:http://localhost:5678}
+ # Optional webhook authentication — configure to match your n8n Webhook node auth settings.
+ # Supported types: basic (username+password), header (custom header), bearer (token).
+ # webhook-auth:
+ # type: header
+ # name: ${N8N_WEBHOOK_AUTH_NAME}
+ # value: ${N8N_WEBHOOK_AUTH_VALUE}
# use-test-webhook: true switches to /webhook-test — requires clicking "Listen for Test Event"
# in the n8n UI first; fires only once. Use for single-trigger manual testing only.
use-test-webhook: false