Webhook Authentication¶
This guide covers how to authenticate incoming webhooks from external services (GitHub, Linear, Stripe, etc.) and associate them with a service account principal.
Overview¶
External services send webhooks to notify our backend of events. These requests must be:
- Validated - Ensure the request actually comes from the claimed service
- Authenticated - Associate the request with a service account principal
The WebhookAuthContextProvider handles both concerns.
sequenceDiagram
participant Ext as External Service
participant API as Alan Backend
participant SA as ServiceAccount
Ext->>API: POST /webhooks/...<br/>+ auth header (secret, signature, basic auth...)
API->>API: Validate using configured auth_type
alt Valid
API->>SA: Look up by email
API->>API: set_auth_context(service_account)
API-->>Ext: 200 OK
else Invalid
API-->>Ext: 403 Forbidden
end
Configuration¶
Register WebhookAuthContextProvider on your blueprint:
from shared.iam.auth_context.providers.webhook import (
WebhookAuthContextProvider,
WebhookAuthType,
)
github_blueprint = CustomBlueprint(
"github_webhooks",
"github_webhooks",
url_prefix="/webhooks/github",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256,
header_name="X-Hub-Signature-256",
secret_name_config_key="GITHUB_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="github-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Parameters¶
| Parameter | Description |
|---|---|
auth_type |
Validation method (see Supported Authentication Types) |
header_name |
HTTP header containing the secret or signature |
secret_name_config_key |
Config key for the secret (stored in Secrets Manager) |
auth_principal_type |
Principal class to use (typically ServiceAccount) |
auth_principal_email |
Email of the service account to authenticate as |
Supported Authentication Types¶
| Type | Signature Format | Use Case |
|---|---|---|
basic |
Basic <base64(user:pass)> |
Datadog |
secret |
Raw secret value | Simple internal webhooks |
sha1 |
sha1=<hex_digest> |
Intercom |
sha256 |
sha256=<hex_digest> |
GitHub, Stripe |
sha256_raw |
<hex_digest> (no prefix) |
Linear |
sha256_base64 |
<base64_digest> |
DocuSign |
svix |
v1,<base64_digest> |
Incident.io, Svix-based services |
WebhookAuthType.basic¶
HTTP Basic Authentication. Credentials stored as JSON {username, password} in Secrets Manager.
WebhookAuthContextProvider(
auth_type=WebhookAuthType.basic,
header_name="Authorization",
secret_name_config_key="DATADOG_WEBHOOK_CREDENTIALS", # JSON: {"username": "...", "password": "..."}
auth_principal_type=ServiceAccount,
auth_principal_email="datadog-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: Datadog webhooks, services using HTTP Basic Auth.
WebhookAuthType.secret¶
Simple shared secret comparison. The header value must exactly match the configured secret.
WebhookAuthContextProvider(
auth_type=WebhookAuthType.secret,
header_name="X-Webhook-Secret",
secret_name_config_key="MY_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="my-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: Simple internal webhooks or services that only support shared secrets.
Security Consideration
Shared secrets are less secure than cryptographic signatures. Prefer HMAC-based types when the external service supports it.
WebhookAuthType.sha1¶
HMAC-SHA1 signature with sha1= prefix.
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha1,
header_name="X-Hub-Signature",
secret_name_config_key="INTERCOM_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="intercom-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: Intercom webhooks.
Signature format: sha1=<hex_digest>
WebhookAuthType.sha256¶
HMAC-SHA256 signature with sha256= prefix.
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256,
header_name="X-Hub-Signature-256",
secret_name_config_key="GITHUB_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="github-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: GitHub, Stripe, and most modern webhook providers.
Signature format: sha256=<hex_digest>
WebhookAuthType.sha256_raw¶
HMAC-SHA256 signature as raw hex digest (no prefix).
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256_raw,
header_name="Linear-Signature",
secret_name_config_key="LINEAR_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="linear-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: Linear webhooks.
Signature format: <hex_digest> (no prefix)
WebhookAuthType.sha256_base64¶
HMAC-SHA256 signature as base64-encoded digest.
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256_base64,
header_name="X-DocuSign-Signature-1",
secret_name_config_key="DOCUSIGN_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="docusign-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: DocuSign webhooks.
Signature format: <base64_digest>
WebhookAuthType.svix¶
Svix-style signature used by Incident.io and other Svix-based webhook providers.
The message to sign is: {webhook_id}.{webhook_timestamp}.{body}
WebhookAuthContextProvider(
auth_type=WebhookAuthType.svix,
header_name="webhook-signature",
secret_name_config_key="INCIDENTIO_WEBHOOK_SECRET", # Format: "whsec_<base64_key>"
auth_principal_type=ServiceAccount,
auth_principal_email="incidentio-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
Use case: Incident.io, any Svix-based webhook provider.
Required headers: webhook-id, webhook-timestamp, webhook-signature
Secret format: whsec_<base64_key>
Signature format: v1,<base64_digest> (may contain multiple space-separated signatures)
How It Works¶
flowchart TD
A[Incoming Webhook] --> B{Header present?}
B -->|No| C[403 Forbidden]
B -->|Yes| D{Auth type?}
D -->|basic| E[Decode Base64<br/>Compare user:pass]
D -->|secret| F[Compare header<br/>with secret]
D -->|sha1| G[Compute HMAC-SHA1<br/>Compare sha1=digest]
D -->|sha256| H[Compute HMAC-SHA256<br/>Compare sha256=digest]
D -->|sha256_raw| I[Compute HMAC-SHA256<br/>Compare raw hex]
D -->|sha256_base64| J[Compute HMAC-SHA256<br/>Compare base64]
D -->|svix| K[Build msg from headers<br/>Compute HMAC-SHA256<br/>Compare v1,base64]
E --> L{Match?}
F --> L
G --> L
H --> L
I --> L
J --> L
K --> L
L -->|No| C
L -->|Yes| M[Look up ServiceAccount<br/>by email]
M --> N{Found?}
N -->|No| C
N -->|Yes| O[set_auth_context]
O --> P[Request Authenticated]
Service Account Setup¶
Each webhook source needs a dedicated service account:
- Create the service account in GCP (e.g.,
github-webhook@alan-eu-tools.iam.gserviceaccount.com) - Create an
AlanEmployeeentry with appropriate roles/permissions - Configure the webhook secret in the external service and AWS Secrets Manager
# Service account entry
alan_employee = AlanEmployee(
alan_email="github-webhook@alan-eu-tools.iam.gserviceaccount.com",
is_active=True,
# Minimal roles for webhook processing
)
Examples¶
GitHub (sha256)¶
github_blueprint = CustomBlueprint(
"github_webhooks",
"github_webhooks",
url_prefix="/webhooks/github",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256,
header_name="X-Hub-Signature-256",
secret_name_config_key="GITHUB_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="github-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Linear (sha256_raw)¶
linear_blueprint = CustomBlueprint(
"linear_webhooks",
"linear_webhooks",
url_prefix="/webhooks/linear",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256_raw,
header_name="Linear-Signature",
secret_name_config_key="LINEAR_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="linear-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Intercom (sha1)¶
intercom_blueprint = CustomBlueprint(
"intercom_webhooks",
"intercom_webhooks",
url_prefix="/webhooks/intercom",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha1,
header_name="X-Hub-Signature",
secret_name_config_key="INTERCOM_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="intercom-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
DocuSign (sha256_base64)¶
docusign_blueprint = CustomBlueprint(
"docusign_webhooks",
"docusign_webhooks",
url_prefix="/webhooks/docusign",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.sha256_base64,
header_name="X-DocuSign-Signature-1",
secret_name_config_key="DOCUSIGN_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="docusign-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Incident.io (svix)¶
incidentio_blueprint = CustomBlueprint(
"incidentio_webhooks",
"incidentio_webhooks",
url_prefix="/webhooks/incidentio",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.svix,
header_name="webhook-signature",
secret_name_config_key="INCIDENTIO_WEBHOOK_SECRET",
auth_principal_type=ServiceAccount,
auth_principal_email="incidentio-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Datadog (basic)¶
datadog_blueprint = CustomBlueprint(
"datadog_webhooks",
"datadog_webhooks",
url_prefix="/webhooks/datadog",
auth_context_providers=[
WebhookAuthContextProvider(
auth_type=WebhookAuthType.basic,
header_name="Authorization",
secret_name_config_key="DATADOG_WEBHOOK_CREDENTIALS",
auth_principal_type=ServiceAccount,
auth_principal_email="datadog-webhook@alan-eu-tools.iam.gserviceaccount.com",
)
],
)
Best Practices¶
Do¶
- Use HMAC-based authentication (
sha1,sha256,sha256_raw,sha256_base64,svix) when supported - Match the auth type to the external service's format (see examples above)
- Create dedicated service accounts per webhook source
- Give webhook service accounts minimal required permissions
- Store secrets in AWS Secrets Manager, never in code
Don't¶
- Process webhooks without validating signatures
- Share service accounts between different webhook sources
- Use
secretauthentication when HMAC-based options are available - Skip authentication with anonymous endpoints
Audit Trail¶
Authenticated webhooks are fully traceable:
current_auth_context.real_principalis set to the service account- All database changes link to
TransactionAuthContext - Logs include
authnzfield with service account info
See Audit Trail for more details.