Skip to content

Receiving Webhooks

An inbound webhook is a request from a system that has no user session and never will. AuthProxy has two mechanisms for it, and which one applies depends on a single question: do you control the sender?


The sender isUseWhy
A service or application you controlClient credentialsThe sender can exchange credentials at /.cratis/token for a bearer token scoped to a route prefix. The proxy authenticates it, and the application receives a caller it can trust.
A third party — Stripe, GitHub, Slack, an e-sign providerAnonymousPathsThey will not perform a token exchange. They sign the request their own way, and only the application knows how to check it.

If you control the sender, prefer client credentials. It keeps authentication in the proxy, which is what the proxy is for. Reach for AnonymousPaths when the sender’s scheme is not yours to choose.


Declare the specific path the provider posts to:

{
"Cratis": {
"AuthProxy": {
"Services": {
"core": {
"Backend": { "BaseUrl": "http://core:8080/" },
"AnonymousPaths": [ "/api/webhooks/payments" ]
}
}
}
}
}
  • Forwards the request to the service instead of answering it with a sign-in page.
  • Strips inbound x-ms-client-principal, x-ms-client-principal-id, x-ms-client-principal-name and Tenant-ID headers, exactly as it does for every other request. A caller cannot assert an identity.
  • Injects no principal headers, because there is no session.

It does not authenticate the request. A declared path is reachable by anyone who knows the URL. The signature check is the application’s, and it is not optional — without it the endpoint accepts anything that anyone posts to it.

A typical provider signs a request with an HMAC over the raw body plus a timestamp. Verifying it means:

  1. Reading the raw body bytes, before any deserialization.
  2. Recomputing the signature with the shared secret, following that provider’s exact canonical form.
  3. Comparing in constant time.
  4. Rejecting timestamps outside a short window, so a captured request cannot be replayed.

Not an omission — a deliberate boundary:

  • Every provider canonicalizes differently. Stripe signs timestamp.body, GitHub signs the body alone, Slack signs v0:timestamp:body, and they disagree on hex versus base64 and on tolerance windows. Configuring that generically is harder to get right than implementing it once in the application.
  • It requires buffering the body. AuthProxy streams request bodies. Buffering them to compute a signature changes the memory profile of the path and risks altering exactly the bytes being verified.
  • The application needs the raw body regardless, for idempotency and for storing the event, so it is already holding the thing that has to be checked.

  • Name the exact path. Entries are prefixes and cover everything beneath them, so /api/webhooks opens every current and future webhook route, including ones added later by someone who did not know the prefix was public. /api/webhooks/payments opens one.
  • Reject the methods you do not serve. A webhook receiver is almost always POST only.
  • Treat the payload as untrusted until verified, including any identifiers used to look up records.
  • Re-read the declared paths when reviewing the deployment. They are the surface reachable without a session, and they are the shortest list worth auditing.

For the sibling case — a page or API a person reaches without an account, such as a magic-link landing page — see Public application surfaces.