We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
SendGrid webhook authentication with phoenix
For a project I’m currently building I need to verify that the email has been delivered to the user, or bounced. Luckily you can do this via SendGrids webhooks, and it’s even pretty simple
What’s not quite as simple is when you want to verify the webhook request. At least, I’d never even heard of the cryptographic signing method they use. Elliptic Curve Digital Signature Algorithm
But worry not! Erlang’s standard library and Plug has got us covered. Below is the all the coded needed to receive and verify the webhook request.
The Plug
This is where the magic happens.
defmodule MyAppWeb.SendGridWebhookAuth do
import Plug.Conn
def init(options), do: options
def call(conn, _opts) do
case verify_signature(conn) do
:ok ->
conn
:error ->
conn
|> send_resp(403, "Invalid signature")
|> halt()
end
end
defp verify_signature(conn) do
with {:ok, body, conn} = read_body(conn, opts),
{:ok, signature} <- get_signature(conn),
{:ok, timestamp} <- get_timestamp(conn),
{:ok, public_key} <- get_public_key() do
verify(body, signature, timestamp, public_key)
else
_ ->
:error
end
end
defp get_signature(conn) do
case get_req_header(conn, "x-twilio-email-event-webhook-signature") do
[signature] -> {:ok, signature}
_ -> :error
end
end
defp get_timestamp(conn) do
case get_req_header(conn, "x-twilio-email-event-webhook-timestamp") do
[timestamp] -> {:ok, timestamp}
_ -> :error
end
end
defp verify(body, signature, timestamp, public_key) do
payload = timestamp <> body
decoded_signature = Base.decode64!(signature)
case :public_key.verify(payload, :sha256, decoded_signature, public_key) do
true -> :ok
false -> :error
end
end
defp get_public_key() do
token = Application.get_env(:aid, MyApp.Mailer)[:webhook_verification_key]
public_key = "-----BEGIN PUBLIC KEY-----\n#{token}\n-----END PUBLIC KEY-----\n"
case :public_key.pem_decode(public_key) do
[{:SubjectPublicKeyInfo, "", :not_encrypted}] ->
:error
[{:SubjectPublicKeyInfo, der_key, :not_encrypted}] ->
{:ok, :public_key.pem_entry_decode({:SubjectPublicKeyInfo, der_key, :not_encrypted})}
_ ->
:error
end
end
end
The Controller
The controller is very simple, I just receive the POST request, and take the events I care about from the message list.
defmodule MyAppWeb.Webhooks.SendGridController do
use MyAppWeb, :controller
@tracked_events ["delivered", "bounce", "deferred", "dropped"]
def new(conn, %{"_json" => msg_list}) do
msg_list
|> Enum.map(&process_email_event/1)
send_resp(conn, 200, "Ok")
end
def new(conn, params) do
send_resp(conn, 422, "Invalid payload")
end
defp process_email_event(%{"event" => event} = params) when event in @tracked_events do
# Do something with the params
end
defp process_email_event(_), do: :ignore
end
The Router
First we set up the pipeline to use the SendGridWebhookAuth
plug.
pipeline :sendgrid_webhook do
plug :accepts, ["json"]
plug MyAppWeb.SendGridWebhookAuth
end
Then we add the route that we configured SendGrid to use.
scope "/webhook/sendgrid", MyAppWeb do
pipe_through :sendgrid_webhook
post "/", Webhooks.SendGridController, :new
end
And that’s it!