Guide

Auth Hooks

Use HTTP or Postgres Functions to customize your authentication flow

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:

HookAvailable on Plan
Before User CreatedFree, Pro
Custom Access TokenFree, Pro
Send SMSFree, Pro
Send EmailFree, Pro
MFA Verification AttemptTeams and Enterprise
Password Verification AttemptTeams and Enterprise

Zuvo supports 2 ways to configure a hook in your project:

Postgres Function

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.

HTTP Endpoint

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.

SQL

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_admin role to execute the function. The supabase_auth_admin role 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

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:

  • v1 denotes the version of the hook
  • whsec_ 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

HookSuggested Function NameWhen it is calledWhat it Does
Send SMSsend_smsEach time an SMS is sentAllows you to customize message content and SMS Provider
Send Emailsend_emailEach time an Email is sentAllows you to customize message content and Email Provider
Custom Access Tokencustom_access_tokenEach time a new JWT is createdReturns the claims you wish to be present in the JWT.
MFA Verification Attemptmfa_verification_attemptEach 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 Attemptpassword_verification_attemptEach 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.

SQL

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.

HTTP

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.
SQL

The error is a JSON object and has the following properties:

  • error An object that contains information about the error.
    • http_code A number indicating the HTTP code to be returned. If not set, the code is HTTP 500 Internal Server Error.
    • message A 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.

HTTP

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 CodeDescriptionExample Usage
200, 202, 204Valid response, proceedSuccessful processing of the request
403, 400Treated as Internal Server Errors and return a 500 Error CodeMalformed requests or insufficient permissions
429, 503Retry-able errorsTemporary 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)EventNotes
00:00:00Initial AttemptInitial invocation begins.
00:00:02Initial Attempt FailsInitial invocation returns 429 or 503 with non-empty retry-after header.
00:00:04Retry Start #1After 2 sec delay, first retry begins.
00:00:05Retry Timeout #1First 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.