This ticket tracks the issue reported at https://groups.google.com/g/killbilling-users/c/ekfz197aNLM.
Issue : Stripe webhook signature verification is impossible via processNotification
Stripe sends its signature in the Stripe-Signature HTTP header, but processNotification only receives the request body, plugin properties, and a CallContext that doesn't carry HTTP headers. The processNotification method in the Stripe plugin is also completely unimplemented — it's a stub that throws an exception saying "not yet implemented, please contact support@killbill.io".
This is a general design limitation in Kill Bill core, not Stripe-specific — The CallContext is built in Context.createCallContextWithAccountId(), which has the full ServletRequest object but doesn't extract headers or query parameters from it. This means any gateway that uses header-based or query-parameter-based webhook signing (which is the majority of modern gateways) cannot be securely implemented through processNotification alone.
Kill Bill's own outbound webhooks are unsigned — When Kill Bill sends push notifications to registered callback URLs, it doesn't sign the payload. The receiving application has no cryptographic way to verify authenticity, forcing them to either trust the network blindly or make expensive round-trip API calls to independently verify every notification before acting on it.
Further analysis :
The Adyen plugin already works around this limitation — It registers a custom plugin servlet (AdyenNotificationServlet) via PluginAppBuilder.withRouteClass() that receives webhooks at /plugins/adyen-plugin/notification. This gives it access to the full HTTP request. Adyen's HMAC happens to be embedded inside the JSON body (in additionalData.hmacSignature), so the Adyen plugin's processNotification can validate it without HTTP headers. However, the servlet is still necessary for Adyen's Basic Authentication, which does require the Authorization header.
The Stripe plugin has no equivalent notification servlet — StripeActivator only registers StripeHealthcheckServlet and StripeCheckoutServlet. There is no StripeNotificationServlet. Webhook handling was simply never built.
Workaround :
Add a StripeNotificationServlet following the Adyen plugin's pattern — Create a new servlet registered at /notification that receives Stripe webhooks, extracts the Stripe-Signature header from the HTTP request, calls Webhook.constructEvent(payload, sigHeader, endpointSecret) for signature verification, and then processes the payment state transitions. Register it in StripeActivator with .withRouteClass(StripeNotificationServlet.class). This stays entirely within the plugin — no Kill Bill core changes required.
One key difference from the Adyen approach — The Adyen servlet is a thin passthrough that just forwards the body to processNotification, because Adyen's HMAC is inside the body. The Stripe servlet will need to perform signature verification at the servlet level itself, since the Stripe-Signature header is only accessible there. The payment state transition logic can still live in processNotification, but the verified event (not the raw body) should be passed into it.
This ticket tracks the issue reported at https://groups.google.com/g/killbilling-users/c/ekfz197aNLM.
Issue : Stripe webhook signature verification is impossible via processNotification
Stripe sends its signature in the Stripe-Signature HTTP header, but processNotification only receives the request body, plugin properties, and a CallContext that doesn't carry HTTP headers. The processNotification method in the Stripe plugin is also completely unimplemented — it's a stub that throws an exception saying "not yet implemented, please contact support@killbill.io".
This is a general design limitation in Kill Bill core, not Stripe-specific — The CallContext is built in Context.createCallContextWithAccountId(), which has the full ServletRequest object but doesn't extract headers or query parameters from it. This means any gateway that uses header-based or query-parameter-based webhook signing (which is the majority of modern gateways) cannot be securely implemented through processNotification alone.
Kill Bill's own outbound webhooks are unsigned — When Kill Bill sends push notifications to registered callback URLs, it doesn't sign the payload. The receiving application has no cryptographic way to verify authenticity, forcing them to either trust the network blindly or make expensive round-trip API calls to independently verify every notification before acting on it.
Further analysis :
The Adyen plugin already works around this limitation — It registers a custom plugin servlet (AdyenNotificationServlet) via PluginAppBuilder.withRouteClass() that receives webhooks at /plugins/adyen-plugin/notification. This gives it access to the full HTTP request. Adyen's HMAC happens to be embedded inside the JSON body (in additionalData.hmacSignature), so the Adyen plugin's processNotification can validate it without HTTP headers. However, the servlet is still necessary for Adyen's Basic Authentication, which does require the Authorization header.
The Stripe plugin has no equivalent notification servlet — StripeActivator only registers StripeHealthcheckServlet and StripeCheckoutServlet. There is no StripeNotificationServlet. Webhook handling was simply never built.
Workaround :
Add a StripeNotificationServlet following the Adyen plugin's pattern — Create a new servlet registered at /notification that receives Stripe webhooks, extracts the Stripe-Signature header from the HTTP request, calls Webhook.constructEvent(payload, sigHeader, endpointSecret) for signature verification, and then processes the payment state transitions. Register it in StripeActivator with .withRouteClass(StripeNotificationServlet.class). This stays entirely within the plugin — no Kill Bill core changes required.
One key difference from the Adyen approach — The Adyen servlet is a thin passthrough that just forwards the body to processNotification, because Adyen's HMAC is inside the body. The Stripe servlet will need to perform signature verification at the servlet level itself, since the Stripe-Signature header is only accessible there. The payment state transition logic can still live in processNotification, but the verified event (not the raw body) should be passed into it.