-
I'm looking into implementing webhooks with supabase to respond to database updates. But to do this securely, we need to be able to be sure that the request is coming from Supabase, not an impersonator - and that the payload is accurate. Otherwise we have to blindly do whatever we're told by a random request that comes in. Usually this is implemented with some kind of signature header. e.g. Stripe, Twilio - and Persona have a very simple sample implementation that doesn't require any library/SDK. It looks like it'd be possible to use pgcrypto for this. Note: I see that there's a headers section, but pasting in a static secret value there seems not great - at the very least there should be some docs around it. Answer from Supabase support to the above question:
But if anyone has used pgsodium in this way before, or has found some other way to add authentication to webhook requests, it'd be great to see the implementation. Or, this discussion could become a feature request - since webhooks are already providing a convenience wrapper for pg_net, maybe supabase could use pgsodium on the user's behalf and add a |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 26 replies
-
I have the same question, how do we validate that the request is coming from supabase itself? |
Beta Was this translation helpful? Give feedback.
-
@mmkal , did you ever find a solution? Authentication was the first question that popped in mind when I read about webhooks and I've been pretty confounded that I can't find anything. I've been thinking about including an encrypted key on the table itself that I could then verify in my edge function but I'm not sure the webhook would send the decrypted text in the payload. |
Beta Was this translation helpful? Give feedback.
-
I ran into that issue myself, so I created a little video to explain how I solved this: https://www.youtube.com/watch?v=iyTfdYVIQiE. You can find the associated article here as well: https://valentinprugnaud.dev/articles/setup-webhook-signature-with-supabase |
Beta Was this translation helpful? Give feedback.
-
That looks like the easiest option but how the hell is it safe to do that in a public schema? It ends up as as a trigger on the table: create trigger my_webhook
after insert on my_table for each row
execute function supabase_functions.http_request (
'https://xxx.supabase.co/functions/v1/my-edge-func',
'POST',
'{"Content-type":"application/json","Authorization":"Bearer eyJhbxxxxxxxxxxx"}',
'{}',
'5000'
); Do users have no way of inspecting the schema authenticated with the anon key? |
Beta Was this translation helpful? Give feedback.
-
Hey all, Thanks for the candid feedback and constructive approaches. The feedback has been passed on to the team for @supabase/edge-functions and they are looking at ways to address this! |
Beta Was this translation helpful? Give feedback.
-
I have been thinking about this the last few days, and one extra possible solution, and possibly nicer than using the service_role key, would be to setup the webhook with the same auth header as the current postgREST session has, if the trigger was executed due to a postgREST call. The trigger should be happening in the same transaction/session as the postgREST call, which would mean that current_setting('request.headers') will be set to a json of all headers. So the code below will retrieve the current Authorization header into the variable
So basically the webhook would be called somewhat like 'security invoker' rules inside postgres would call a function, with the JWT of the request triggering the original query that triggered the webhook to run. I must admit, I did not test this, but as long as the trigger originated from a PostgREST initiated session, I do not see why this would not be the case? I will do some tests and report back if someone is interested. I am also working on a nicer way to "handle" webhooks in general on our implementations, will share this in the coming days once finished with the community. |
Beta Was this translation helpful? Give feedback.
-
I'm trying this approach, which I've copied from my comment on https://github.com/orgs/supabase/discussions/12813#discussioncomment-10624304; it implements signatures like the custom auth hooks. -- Thanks, this was a great start. I saw that the custom auth hook uses the webhook standard which makes the verification code in the hook very simple and also protects against message modification (content is signed) and replay (to some extent) via timestamps. Using First we can get a shared secret using select encode(pgsodium.crypto_auth_hmacsha256_keygen(), 'base64'); Then we can store that secret value in vault, along with the project URL (I've put these both in my seed, and populated them separately in production, so that I can use this in local dev). select vault.create_secret(
-- not my secret for anything, just generated for example
'qHUp608CcaSPRRZVUNbf6cke3M2PgRZkrCWg3UPeNMY=',
'my_function_secret',
'shared secret for my-function'
);
select vault.create_secret(
'http://host.docker.internal:54321',
'project_url',
'supabase project url'
); Then we can create a trigger like this to e.g. push through inserts. create or replace function public.call_edge_function_from_trigger()
returns trigger
set search_path = ''
as $$
declare
function_name text;
secret_name text;
project_url text;
full_url text;
webhook_timestamp text;
webhook_payload jsonb;
begin
function_name := tg_argv[0];
secret_name := tg_argv[1];
select floor(extract(epoch from now()))::bigint::text into webhook_timestamp;
project_url := private.get_secret('project_url');
full_url := project_url || '/functions/v1/' || function_name;
webhook_payload := jsonb_build_object(
'type', tg_op,
'table', tg_table_name,
'schema', tg_table_schema,
'record', new,
'old_record', old
);
-- call the http_request function with the constructed url
perform net.http_post(
url := full_url,
headers := jsonb_build_object(
'content-type', 'application/json',
'webhook-id', function_name,
'webhook-timestamp', webhook_timestamp,
'webhook-signature', 'v1,' || encode(
pgsodium.crypto_auth_hmacsha256(
(function_name || '.' || webhook_timestamp || '.' || webhook_payload::text)::bytea,
decode(private.get_secret(secret_name), 'base64')
),
'base64'
)
),
body := webhook_payload,
timeout_milliseconds := 5000
);
return null;
end;
$$ language plpgsql;
drop trigger if exists send_board_match_notifications on public.board;
create trigger send_board_match_notifications
after insert on public.mytable
for each row
execute function public.call_edge_function_from_trigger(
'my-function',
'my_function_secret'
); And on the Deno edge function side (presumably also usable in some form elsewhere too) we have something like this to verify it, assuming that the same base4 encoded secret has been saved in a function secret import { Webhook } from 'https://esm.sh/[email protected]'
import { createClient } from '@supabase/supabase-js'
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { ok, serverError } from '../_shared/responses.ts'
console.log("Hello from Functions!")
Deno.serve(async (req) => {
const payload = await req.text()
const base64_secret = Deno.env.get('MY_FUNCTION_SECRET_SECRET') ?? ''
const headers = Object.fromEntries(req.headers)
console.log(headers)
const wh = new Webhook(base64_secret)
try {
const verified = wh.verify(payload, headers)
console.log(verified)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
)
return ok({ message: 'ok' })
} catch (error) {
console.error('Unhandled error', error)
return serverError({
error: `Failed to process the request: ${error}`,
})
}
}) Implementation details taken from the JS |
Beta Was this translation helpful? Give feedback.
-
I have integrated Supabase with Clerk and set up webhooks for user events like user.created and user.updated. While other CRUD operations are functioning correctly with the Clerk token, I am encountering an issue during webhook events where the Clerk token is not being sent. For existing users, everything works perfectly, and the token is received. However, during the webhook events, I receive the following error from Supabase:
This error seems to be related to the missing Clerk token during the webhook flow, despite the route being set up correctly. The issue only arises with webhook events, not with other operations. Here is the relevant code snippet for creating the Supabase client: route.js for webhook event capturing
In the webhook setup, I correctly verify the event and headers, but the missing Clerk token is causing the JWT error. Could you help investigate why the token is not being sent during webhook events and how to resolve this? Here's the complete code for better context:
server action :
Please can anyone help ? |
Beta Was this translation helpful? Give feedback.
-
I faced this issue yesterday as I was trying to create vector embeddings after a table row is updated or created and had a webhook that called an edge function. Based on this tutorial, I added the anon key as Authorization header when setting up the webhook with "Bearer " as the initial in the value of the header. Then it worked fine. I think the docs should be a bit more clearer and updated as I couldn't figure out what the jwt value is until I watched the video, which is the anon key itself. |
Beta Was this translation helpful? Give feedback.
Not too familiar with Deno (that's why I went with a Nest.js API instead), but a quick Chat-GPT conversation gives me something like this: