What is a hook
A hook is an endpoint that allows you to alter the default Zuvo Auth flow at specific execution points. Developers can use hooks to add custom behavior that's not supported natively.
Hooks help you:
- Track the origin of user signups by adding metadata
- Improve security by adding additional checks to password and multi-factor authentication
- Support legacy systems by integrating with identity credentials from external authentication systems
- Add additional custom claims to your JWT
- Send authentication emails or SMS messages through a custom provider
The following hooks are available:
| Hook | Available on Plan |
|---|---|
| Before User Created | Free, Pro |
| Custom Access Token | Free, Pro |
| Send SMS | Free, Pro |
| Send Email | Free, Pro |
| MFA Verification Attempt | Teams and Enterprise |
| Password Verification Attempt | Teams and Enterprise |
Zuvo supports 2 ways to configure a hook in your project:
A Postgres function can be configured as a hook. The function should take in a single argument -- the event of type JSONB -- and return a JSONB object. Since the Postgres function runs on your database, the request does not leave your project's instance.
An HTTP Hook is an endpoint which takes in a JSON event payload and returns a JSON response. You can use any HTTP endpoint as a Hook, including an endpoint in your application. The easiest way to create an HTTP hook is to create a Zuvo Edge Function.
Security model
Sign the payload and grant permissions selectively in order to guard the integrity of the payload.
When you configure a Postgres function as a hook, Zuvo will automatically apply the following grants to the function for these reasons:
- Allow the
supabase_auth_adminrole to execute the function. Thesupabase_auth_adminrole is the Postgres role that is used by Zuvo Auth to make requests to your database. - Revoke permissions from other roles (e.g.
anon,authenticated,public) to ensure the function is not accessible by Zuvo Data APIs.
-- Grant access to function to supabase_auth_admin
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
-- Grant access to schema to supabase_auth_admin
grant usage on schema public to supabase_auth_admin;
-- Revoke function permissions from authenticated, anon and public
revoke execute
on function public.custom_access_token_hook
from authenticated, anon, public;
You will need to alter your row-level security (RLS) policies to allow the supabase_auth_admin role to access tables that you have RLS policies on. You can read more about RLS policies here.
Alternatively, you can create your Postgres function via the dashboard with the security definer tag. The security definer tag specifies that the function is to be executed with the privileges of the user that owns it.
Currently, functions created via the dashboard take on the postgres role. Read more about the security definer tag in our database guide
HTTP Hooks in Zuvo follow the Standard Webhooks Specification, which is a set of guidelines aligning how hooks are implemented. The specification attaches three security headers to guarantee the integrity of the payload:
webhook-id: the unique webhook identifier described in the preceding sections.webhook-timestamp: integer UNIX timestamp (seconds since epoch).webhook-signature: the signatures of this webhook. This is generated from body of the hook.
When the request is made to the HTTP hook, you should use the Standard Webhooks libraries to verify these headers.
When an HTTP hook is created, the secret generated should be of the v1,whsec_<base64-secret> format:
v1denotes the version of the hookwhsec_signifies that the secret is symmetric<base64-secret>implies a Standard Base64 encoded secret which can contain the characters+,/and=
The secret is used to verify the payload received in your hook. Create an entry in your .env.local file to store the <standard-base64-secret> portion of the secret for each hook that you have. For example:
SEND_SMS_HOOK_SECRETS=v1,whsec_<base64-secret>
There field is expressed in plural rather than singular as there are plans to allow for asymmetric signing and multiple hook secrets for ease of secret rotation. For instance: <standard-base-64-secret>|<another-standard-base-64-secret>.
Use the secret in conjunction with the Standard Webhooks package to verify the payload before processing it:
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
Deno.serve(async (req) => {
const payload = await req.text()
const hookSecret = Deno.env.get('SEND_SMS_HOOK_SECRETS').replace('v1,whsec_', '')
// Extract headers and security specific fields
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(hookSecret)
const data = wh.verify(payload, headers)
// Payload data is verified, continue with business logic here
// ...
})
Using Hooks
Developing
Let us develop a Hook locally and then deploy it to the cloud. As a recap, here’s a list of available Hooks
| Hook | Suggested Function Name | When it is called | What it Does |
|---|---|---|---|
| Send SMS | send_sms | Each time an SMS is sent | Allows you to customize message content and SMS Provider |
| Send Email | send_email | Each time an Email is sent | Allows you to customize message content and Email Provider |
| Custom Access Token | custom_access_token | Each time a new JWT is created | Returns the claims you wish to be present in the JWT. |
| MFA Verification Attempt | mfa_verification_attempt | Each time a user tries to verify an MFA factor. | Returns a decision on whether to reject the attempt and future ones, or to allow the user to keep trying. |
| Password Verification Attempt | password_verification_attempt | Each time a user tries to sign in with a password. | Return a decision whether to allow the user to reject the attempt, or to allow the user to keep trying. |
Edit config.toml to set up the Auth Hook locally.
Modify the auth.hook.<hook_name> field and set uri to a value of pg-functions://postgres/<schema>/<function_name>
[auth.hook.<hook_name>]
enabled = true
uri = "pg-functions://...."
You need to assign additional permissions so that Zuvo Auth can access the hook as well as the tables it interacts with.
The supabase_auth_admin role does not have permissions to the public schema. You need to grant the role permission to execute your hook:
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
You also need to grant usage to supabase_auth_admin:
grant usage on schema public to supabase_auth_admin;
Also revoke permissions from the authenticated and anon roles to ensure the function is not accessible by Zuvo Serverless APIs.
revoke execute
on function public.custom_access_token_hook
from authenticated, anon;
For security, we recommend against the use the security definer tag. The security definer tag specifies that the function is to be executed with the privileges of the user that owns it. When a function is created via the Zuvo dashboard with the tag, it will have the extensive permissions of the postgres role which make it easier for undesirable actions to occur.
We recommend that you do not use any tag and explicitly grant permissions to supabase_auth_admin as described above.
Read more about security definer tag in our database guide.
Once done, save your Auth Hook as a migration in order to version the Auth Hook and share it with other team members. Run supabase migration new to create a migration.
Here is an example hook signature:
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
as $$
declare
-- Insert variables here
begin
-- Insert logic here
return event;
end;
$$;
You can visit SQL Editor > Templates for hook templates.
Modify the auth.hook.<hook_name> field and set uri to a valid HTTP URI. For example, the send_sms hook would take the following fields:
[auth.hook.send_sms]
enabled = true
uri = "http://host.docker.internal:54321/functions/v1/send_sms"
# Comma separated list of secrets
secrets = "env(SEND_SMS_HOOK_SECRETS)"
Fill in the Hook Secret in supabase/functions/.env
SEND_SMS_HOOK_SECRETS='v1,whsec_<base64-secret>'
Start the function locally:
supabase functions serve send-sms --no-verify-jwt
Disable JWT verification via the --no-verify-jwt to accommodate hooks which are run before a JWT is issued. Payload authenticity is instead protected via the appended security headers associated with the Standard Webhooks Standard.
Note that payloads are sent uncompressed in order to accurately track Content Length. In addition, there is a 20KB payload limit to guard against payload stuffing attacks.
Deploying
In the dashboard, navigate to Authentication > Hooks and select the appropriate function type (SQL or HTTP) from the dropdown menu.
Error handling
You should return an error when facing a runtime error. Runtime errors are specific to your application and arise from specific business rules rather than programmer errors.
Runtime errors could happen when:
- The user does not have appropriate permissions
- The event payload received does not have required claims.
- The user has performed an action which violates a business rule.
- The email or phone provider used in the webhook returned an error.
The error is a JSON object and has the following properties:
errorAn object that contains information about the error.http_codeA number indicating the HTTP code to be returned. If not set, the code is HTTP 500 Internal Server Error.messageA message to be returned in the HTTP response. Required.
Here's an example:
{
"error": {
"http_code": 429,
"message": "You can only verify a factor once every 10 seconds."
}
}
Errors returned from a Postgres Hook are not retry-able. When an error is returned, the error is propagated from the hook to Zuvo Auth and translated into an HTTP error which is returned to your application. Zuvo Auth will only take into account the error and disregard the rest of the payload.
Hooks return status codes based on the nature of the response. These status codes help determine the next steps in the processing flow:
| HTTP Status Code | Description | Example Usage |
|---|---|---|
| 200, 202, 204 | Valid response, proceed | Successful processing of the request |
| 403, 400 | Treated as Internal Server Errors and return a 500 Error Code | Malformed requests or insufficient permissions |
| 429, 503 | Retry-able errors | Temporary server overload or maintenance |
Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a 429 or 503 status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds. We have a time budget of 5s for the entire webhook invocation, including retry requests.
Here's a sample HTTP retry schedule:
| Time Since Start (HH:MM:SS) | Event | Notes |
|---|---|---|
| 00:00:00 | Initial Attempt | Initial invocation begins. |
| 00:00:02 | Initial Attempt Fails | Initial invocation returns 429 or 503 with non-empty retry-after header. |
| 00:00:04 | Retry Start #1 | After 2 sec delay, first retry begins. |
| 00:00:05 | Retry Timeout #1 | First retry times out, exceeded 5 second budget and invocation returns an error. |
Return a retry-able error by attaching a appropriate status code (429, 503) and a non-empty retry-after header
return new Response(
JSON.stringify({
error: `Failed to process the request: ${error}`,
}),
{ status: 429, headers: { 'Content-Type': 'application/json', 'retry-after': 'true' } }
)
Note that all responses, including error responses, need a Content-Type of application/json - not specifying the appropriate Content-Type will result in the function returning an error response. Zuvo Auth will in turn return an Internal Server Error.
Outside of runtime errors, both HTTP Hooks and Postgres Hooks return timeout errors. Postgres Hooks have
Log field reference tables are available in Zuvo Studio.
seconds to complete processing while HTTP Hooks should complete in
Log field reference tables are available in Zuvo Studio.
seconds. Both HTTP Hooks and Postgres Hooks are run in a transaction do limit the duration of execution to avoid delays in authentication process.
Available Hooks
Each Hook description contains an example JSON Schema which you can use in conjunction with JSON Schema Faker in order to generate a mock payload. For HTTP Hooks, you can also use the Standard Webhooks Testing Tool to simulate a request.