It seems impossible to implement the Stripe webhooks securely using the processNotification method alone for webhook responses.
To implement the webhooks properly, the receiver needs to validate the signature, which the Stripe gateway puts in a custom header called Stripe-Signature. However, headers and query parameters are not available in the call context within processNotification. Any serious webhook is going to require signing, either in the header, as query parameters, or in the body. At present, the first two seem impossible, and only the last scenario can be accommodated.
This is the problem, in the Kill Bill source code:
killbill/jaxrs/src/main/java/org/killbill/billing/jaxrs/util/Context.java
Class: org.killbill.billing.jaxrs.util.Context
public CallContext createCallContextWithAccountId(final UUID accountId, final String createdBy, final String reason, final String comment, final ServletRequest request)
throws IllegalArgumentException {
try {
Preconditions.checkNotNull(createdBy, String.format("Header %s needs to be set", JaxrsResource.HDR_CREATED_BY));
final Tenant tenant = getTenantFromRequest(request);
final UUID tenantId = tenant == null ? null : tenant.getId();
final CallContext callContext = contextFactory.createCallContext(accountId, tenantId, createdBy, origin, userType, reason,
comment, getOrCreateUserToken());
populateMDCContext(callContext);
return callContext;
} catch (final NullPointerException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
This is the key point where everything is available.
This method constructs the context, and actually has the request object to extract the headers and query parameters and inject them into the call context, but does not do it. The result is that there is insufficient call context inside of processNotification to implement a response to any reasonably structured gateway notice using this mechanism.
A simple modification of this class to store the headers and query parameters would provide the required information in the call context which would get passed along to the user implementation of processNotification(), solving the problem.
Workarounds:
There are only two I have been able to find:
1) Implement a plugin servlet. This is probably the best method. But it is unfortunate there is this great default mechanism that avoids the extra complexity of wiring up a servlet, and yet it can't be used with real-world gateways due to a very simple omission.
2) Create a request forwarding service that reformats the request to either stuff the required headers into the message body, or into query parameters. The latter is also not officially available in the processNotification context, but can be pulled from the MDC, though that seems like a bad idea.
If passing all headers and the query string in the call context is undesirable for some reason, then a configuration property that allows a passthrough specification would suffice, though require additional mandatory config when installing the plugin.
My application actually uses a forwarder for other reasons, and I have temporarily adapted it per (2) as proof of concept. It works, but I am dissatisfied with the solution. Requiring such a translator service for a normal gateway adapter to function seems to impose unnecessary complexity and is inappropriate for any serious plugin implementation.
BTW - If I am missing something obvious, please let me know :)
Related issue: There is a related issue with Kill Bill's own webhooks. It does not seem to sign the message contents for the URL calls. So the user is left to implement some scheme which is rather difficult for the application to do. The only control is through the url, but it is tricky to truly authenticate through a static url. Just signing the messages would probably be sufficient, as Stripe is doing. So far, the only workarounds I see are to secure the network between the app and killbill and trust everyone, which is not great, or when a message is received, to verify all of the information independently. But this significantly increases the api hits, and all this work must be done before the message can be discarded as malicious.