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?
Choosing the mechanism
Section titled “Choosing the mechanism”| The sender is | Use | Why |
|---|---|---|
| A service or application you control | Client credentials | The 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 provider | AnonymousPaths | They 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.
Third-party webhooks
Section titled “Third-party webhooks”Declare the specific path the provider posts to:
{ "Cratis": { "AuthProxy": { "Services": { "core": { "Backend": { "BaseUrl": "http://core:8080/" }, "AnonymousPaths": [ "/api/webhooks/payments" ] } } } }}What AuthProxy does
Section titled “What AuthProxy does”- 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-nameandTenant-IDheaders, exactly as it does for every other request. A caller cannot assert an identity. - Injects no principal headers, because there is no session.
What AuthProxy does not do
Section titled “What AuthProxy does not do”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:
- Reading the raw body bytes, before any deserialization.
- Recomputing the signature with the shared secret, following that provider’s exact canonical form.
- Comparing in constant time.
- Rejecting timestamps outside a short window, so a captured request cannot be replayed.
Why the proxy does not do this for you
Section titled “Why the proxy does not do this for you”Not an omission — a deliberate boundary:
- Every provider canonicalizes differently. Stripe signs
timestamp.body, GitHub signs the body alone, Slack signsv0: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.
Keeping the opening small
Section titled “Keeping the opening small”- Name the exact path. Entries are prefixes and cover everything beneath them, so
/api/webhooksopens every current and future webhook route, including ones added later by someone who did not know the prefix was public./api/webhooks/paymentsopens one. - Reject the methods you do not serve. A webhook receiver is almost always
POSTonly. - 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.