Gaiia webhook: verify secret via query param instead of signature header

This commit is contained in:
Graham McIntire 2026-02-14 16:08:27 -06:00
parent 82dd75d2e9
commit 95d833874a

View file

@ -2,8 +2,13 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
@moduledoc """
Webhook endpoint for receiving real-time updates from Gaiia.
Gaiia sends unsigned webhooks via axios. We verify the organization exists
and has an active Gaiia integration, then process the event.
Gaiia doesn't sign webhook requests. Instead, a shared secret is verified
via a `secret` query parameter in the webhook URL. When configuring the
webhook in Gaiia, use:
https://towerops.net/api/v1/webhooks/gaiia/<org_id>?secret=<webhook_secret>
If no webhook_secret is configured in the integration, all requests are accepted.
Route: POST /api/v1/webhooks/gaiia/:organization_id
"""
@ -14,8 +19,9 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
require Logger
def create(conn, %{"organization_id" => organization_id}) do
with {:ok, _integration} <- get_gaiia_integration(organization_id),
def create(conn, %{"organization_id" => organization_id} = params) do
with {:ok, integration} <- get_gaiia_integration(organization_id),
:ok <- verify_secret(integration, params),
{:ok, event} <- get_event(conn.body_params) do
# Gaiia sends object data in "payload" field
data = Map.get(conn.body_params, "payload", Map.get(conn.body_params, "data", %{}))
@ -35,6 +41,24 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
end
end
defp verify_secret(integration, params) do
case get_in(integration.credentials, ["webhook_secret"]) do
nil ->
# No secret configured — accept all requests
:ok
"" ->
:ok
expected_secret ->
if Plug.Crypto.secure_compare(params["secret"] || "", expected_secret) do
:ok
else
{:error, :invalid_secret}
end
end
end
defp get_event(%{"eventName" => event}) when is_binary(event), do: {:ok, event}
defp get_event(%{"event" => event}) when is_binary(event), do: {:ok, event}
defp get_event(_), do: {:error, :missing_event}
@ -45,6 +69,9 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Gaiia integration not found"})
{:error, :invalid_secret} ->
conn |> put_status(:unauthorized) |> json(%{error: "Invalid webhook secret"})
{:error, :missing_event} ->
conn |> put_status(:bad_request) |> json(%{error: "Missing event field"})