Skip to content

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:

  1. Validated - Ensure the request actually comes from the claimed service
  2. 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
Hold "Alt" / "Option" to enable pan & zoom

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]
Hold "Alt" / "Option" to enable pan & zoom

Service Account Setup

Each webhook source needs a dedicated service account:

  1. Create the service account in GCP (e.g., github-webhook@alan-eu-tools.iam.gserviceaccount.com)
  2. Create an AlanEmployee entry with appropriate roles/permissions
  3. 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 secret authentication when HMAC-based options are available
  • Skip authentication with anonymous endpoints

Audit Trail

Authenticated webhooks are fully traceable:

  • current_auth_context.real_principal is set to the service account
  • All database changes link to TransactionAuthContext
  • Logs include authnz field with service account info

See Audit Trail for more details.