towerops/lib/towerops_web/plugs/webhook_auth.ex
Graham McIntire ea91dae0e6 fix: 6 medium-severity bugs (M2, M8, M9, M10, M11, M16)
- M2: get_user_by_email/1 uses lower(email) = lower(?) so User@Example.com
  and user@example.com resolve to the same account; closes a lookalike-
  registration / password-reset confusion risk.
- M8: webhook auth plug no longer echoes "Webhook authentication not
  configured" — the misconfiguration is logged server-side and the caller
  gets a generic Internal server error so endpoints can't be probed.
- M9: coverages controller logs the underlying KMZ build error and
  returns a generic "Failed to build KMZ" string — filesystem paths and
  zip internals no longer leak.
- M10: account-data controller no longer crashes with MatchError when an
  org has zero or multiple memberships; defaults to "member" and treats
  any owner membership as owner.
- M11: ActivityController switches to a hard whitelist
  (Atom.to_string/1 comparison) instead of String.to_existing_atom/1;
  unknown filter types are dropped silently and the atom table can't be
  grown from API input.
- M16: title-tracking MutationObserver is stored on `this` so destroyed()
  actually disconnects it — fixes a memory leak per LV navigation.
2026-05-12 11:38:13 -05:00

57 lines
1.5 KiB
Elixir

defmodule ToweropsWeb.Plugs.WebhookAuth do
@moduledoc """
Plug for authenticating webhook requests using a shared secret.
Expects the Authorization header to contain a Bearer token matching
the configured `agent_webhook_secret`.
Returns 401 Unauthorized if:
- No Authorization header is present
- Token does not match the configured secret
"""
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
verify_secret(conn, token)
_ ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Missing or invalid Authorization header"})
|> halt()
end
end
defp verify_secret(conn, token) do
secret = Application.get_env(:towerops, :agent_webhook_secret)
cond do
is_nil(secret) or secret == "" ->
require Logger
Logger.error("agent_webhook_secret not configured — rejecting webhook request")
# Log the real reason server-side; don't echo it to the caller —
# otherwise an attacker can probe for unconfigured endpoints.
conn
|> put_status(:internal_server_error)
|> json(%{error: "Internal server error"})
|> halt()
Plug.Crypto.secure_compare(token, secret) ->
conn
true ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Invalid webhook secret"})
|> halt()
end
end
end