Auth

Auth Hooks

Supabase allows you to use Postgres functions to alter the default Supabase Auth flow. 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

You can use hooks at specific points along an Auth flow to perform custom behavior via Postgres Functions. The following hooks are available:

HookAvailable on Plan
Custom Access TokenFree, Pro
MFA Verification AttemptTeams and Enterprise
Password Verification AttemptTeams and Enterprise

You can connect a hook to Supabase Auth to signal to Supabase Auth that it should make use of the hook.

Create a hook

What is a hook

A hook is a Postgres Function with a single argument -- the event of type JSONB -- and which returns a JSONB object.

This function is invoked by Supabase Auth at its point in the flow by providing an event object. The object returned by the function gives instructions on how Supabase Auth should continue processing.

To access properties of the event argument, you can use the JSON operators and functions.

There are no restrictions as to what language can be used to write Auth Hooks. If PL/pgSQL is too difficult consider using the plv8 extension which lets you use JavaScript to define functions.

Here is an example hook signature:


_11
create or replace function public.custom_access_token_hook(event jsonb)
_11
returns jsonb
_11
language plpgsql
_11
as $$
_11
declare
_11
-- Insert variables here
_11
begin
_11
-- Insert logic here
_11
return event;
_11
end;
_11
$$;

You can visit SQL Editor > Templates for hook templates.

Hook grants and roles

You need to assign additional permissions so that Supabase 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 function:


_10
grant execute
_10
on function public.custom_access_token_hook
_10
to supabase_auth_admin;

You also need to grant usage to supabase_auth_admin:


_10
grant usage on schema public to supabase_auth_admin;

Also revoke permissions from the authenticated, public, and anon roles to ensure the function is not accessible by Supabase Serverless APIs. The public role has access to functions created on public.* by default and anon and authenticated inherit permissions from the public role. Permission is revoked from thepublic role in order to prevent the anon and authenticated roles from inheriting permissions to invoke the Postgres Hook.


_10
revoke execute
_10
on function public.custom_access_token_hook
_10
from authenticated, anon, public;

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

Hook errors

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 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:


_10
{
_10
"error": {
_10
"http_code": 429,
_10
"message": "You can only verify a factor once every 10 seconds."
_10
}
_10
}

When an error is returned, the error is propagated from the hook to Supabase Auth and translated into a HTTP error which is returned to your application. Supabase Auth will only take into account the error and disregard the rest of the payload.

Timeouts

Ensure that your hooks complete within 2 seconds to avoid any errors.

Connect a hook

In the dashboard, navigate to Authentication > Hooks (Beta) and select the appropriate PostgreSQL function from the dropdown menu.

Hook: MFA verification attempt

You can add additional checks to the Supabase MFA implementation with hooks. For example, you can:

  • Limit the number of verification attempts performed over a period of time.
  • Sign out users who have too many invalid verification attempts.
  • Count, rate limit, or ban sign-ins.

Inputs

Supabase Auth will supply the following fields to your hook:

  • factor_id Unique identifier for the MFA factor being verified.
  • user_id Unique identifier for the user.
  • valid Whether the verification attempt was valid. For TOTP, this means that the six digit code was correct (true) or incorrect (false).

Example payload:


_10
{
_10
"factor_id": "6eab6a69-7766-48bf-95d8-bd8f606894db",
_10
"user_id": "3919cb6e-4215-4478-a960-6d3454326cec",
_10
"valid": true
_10
}

Outputs

Return this if your hook processed the input without errors.

  • decision A string containing the decision on whether to allow authentication to move forward. Use reject to deny the verification attempt and log the user out of all active sessions. Use continue to use the default Supabase Auth behavior.
  • message The message to show the user if the decision was reject.

Example output:


_10
{
_10
"decision": "reject",
_10
"message": "You have exceeded maximum number of MFA attempts."
_10
}

Examples

Your company requires that a user can input an incorrect MFA Verification code no more than once every 2 seconds.

Create a table to record the last time a user had an incorrect MFA verification attempt for a factor.


_10
create table public.mfa_failed_verification_attempts (
_10
user_id uuid not null,
_10
factor_id uuid not null,
_10
last_failed_at timestamp not null default now(),
_10
primary key (user_id, factor_id)
_10
);

Create a hook to read and write information to this table. For example:


_66
create function public.hook_mfa_verification_attempt(event jsonb)
_66
returns jsonb
_66
language plpgsql
_66
as $$
_66
declare
_66
last_failed_at timestamp;
_66
begin
_66
if event->'valid' is true then
_66
-- code is valid, accept it
_66
return jsonb_build_object('decision', 'continue');
_66
end if;
_66
_66
select last_failed_at into last_failed_at
_66
from public.mfa_failed_verification_attempts
_66
where
_66
user_id = event->'user_id'
_66
and
_66
factor_id = event->'factor_id';
_66
_66
if last_failed_at is not null and now() - last_failed_at < interval '2 seconds' then
_66
-- last attempt was done too quickly
_66
return jsonb_build_object(
_66
'error', jsonb_build_object(
_66
'http_code', 429,
_66
'message', 'Please wait a moment before trying again.'
_66
)
_66
);
_66
end if;
_66
_66
-- record this failed attempt
_66
insert into public.mfa_failed_verification_attempts
_66
(
_66
user_id,
_66
factor_id,
_66
last_refreshed_at
_66
)
_66
values
_66
(
_66
event->'user_id',
_66
event->'factor_id',
_66
now()
_66
)
_66
on conflict do update
_66
set last_refreshed_at = now();
_66
_66
-- finally let Supabase Auth do the default behavior for a failed attempt
_66
return jsonb_build_object('decision', 'continue');
_66
end;
_66
$$;
_66
_66
-- Assign appropriate permissions and revoke access
_66
grant execute
_66
on function public.hook_mfa_verification_attempt
_66
to supabase_auth_admin;
_66
_66
grant all
_66
on table public.mfa_failed_verification_attempts
_66
to supabase_auth_admin;
_66
_66
revoke execute
_66
on function public.hook_mfa_verification_attempt
_66
from authenticated, anon, public;
_66
_66
revoke all
_66
on table public.mfa_failed_verification_attempts
_66
from authenticated, anon, public;

Hook: Password verification attempt

Your company wishes to increase security beyond the requirements of the default password implementation in order to fulfill security or compliance requirements. You plan to track the status of a password sign-in attempt and take action via an email or a restriction on logins where necessary.

As this hook runs on unauthenticated requests, malicious users can abuse the hook by calling it multiple times. Pay extra care when using the hook as you can unintentionally block legitimate users from accessing your application.

Check if a password is valid prior to taking any additional action to ensure the user is legitimate. Where possible, send an email or notification instead of blocking the user.

Inputs

  • user_id Unique identifier for the user attempting to sign in. Correlate this to the auth.users table.
  • valid Whether the password verification attempt was valid.

_10
{
_10
"user_id": "3919cb6e-4215-4478-a960-6d3454326cec",
_10
"valid": true
_10
}

Outputs

Return these only if your hook processed the input without errors.

  • decision A string containing the decision whether to allow authentication to move forward. Use reject to completely reject the verification attempt and log the user out of all active sessions. Use continue to use the default Supabase Auth behavior.
  • message The message to show the user if the decision was reject.
  • should_logout_user Whether to logout a user if a reject decision is issued. Has no effect when a continue decision is issued.

Example output:


_10
{
_10
"decision": "reject",
_10
"message": "You have exceeded maximum number of password sign-in attempts.",
_10
"should_logout_user": "false"
_10
}

Examples

As part of new security measures within the company, users can only input an incorrect password every 10 seconds and not more than that. You want to write a hook to enforce this.

Create a table to record each user's last incorrect password verification attempt.


_10
create table public.password_failed_verification_attempts (
_10
user_id uuid not null,
_10
last_failed_at timestamp not null default now(),
_10
primary key (user_id)
_10
);

Create a hook to read and write information to this table. For example:


_62
create function public.hook_password_verification_attempt(event jsonb)
_62
returns jsonb
_62
language plpgsql
_62
as $$
_62
declare
_62
last_failed_at timestamp;
_62
begin
_62
if event->'valid' is true then
_62
-- password is valid, accept it
_62
return jsonb_build_object('decision', 'continue');
_62
end if;
_62
_62
select last_failed_at into last_failed_at
_62
from public.password_failed_verification_attempts
_62
where
_62
user_id = event->'user_id';
_62
_62
if last_failed_at is not null and now() - last_failed_at < interval '10 seconds' then
_62
-- last attempt was done too quickly
_62
return jsonb_build_object(
_62
'error', jsonb_build_object(
_62
'http_code', 429,
_62
'message', 'Please wait a moment before trying again.'
_62
)
_62
);
_62
end if;
_62
_62
-- record this failed attempt
_62
insert into public.password_failed_verification_attempts
_62
(
_62
user_id,
_62
last_failed_at
_62
)
_62
values
_62
(
_62
event->'user_id',
_62
now()
_62
)
_62
on conflict do update
_62
set last_failed_at = now();
_62
_62
-- finally let Supabase Auth do the default behavior for a failed attempt
_62
return jsonb_build_object('decision', 'continue');
_62
end;
_62
$$;
_62
_62
-- Assign appropriate permissions
_62
grant execute
_62
on function public.hook_password_verification_attempt
_62
to supabase_auth_admin;
_62
_62
grant all
_62
on table public.password_failed_verification_attempts
_62
to supabase_auth_admin;
_62
_62
revoke execute
_62
on function public.hook_password_verification_attempt
_62
from authenticated, anon, public;
_62
_62
revoke all
_62
on table public.password_failed_verification_attempts
_62
from authenticated, anon, public;

Hook: Custom access token

The custom access token hook runs before a token is issued and allows you to add additional claims based on the authentication method used.

Claims returned must conform to our specification. Supabase Auth will check for these claims after the hook is run and return an error if they are not present.

These are the fields currently available on an access token:

Required Claims: aud, exp, iat, sub, email, phone, role, aal, session_id Optional Claims: jti, iss, nbf, app_metadata, user_metadata, amr

Inputs

  • user_id Unique identifier for the user attempting to sign in. Correlate this to the auth.users table.
  • claims Claims which are attached to the access token.
  • authentication_method the authentication method used to request the access token. Possible values include: oauth, password, otp, totp, recovery, invite, sso/saml, magiclink, email/signup, email_change, token_refresh.

Outputs

Return these only if your hook processed the input without errors.

  • claims A json containing the updated claims after the hook has been run.

Examples

You can allow registered admin users to perform restricted actions by granting an admin claim to their token.

Create a profiles table with an is_admin flag:


_10
create table profiles (
_10
user_id uuid not null primary key references auth.users (id),
_10
is_admin boolean not null default false
_10
);

Create a hook:


_48
create or replace function public.custom_access_token_hook(event jsonb)
_48
returns jsonb
_48
language plpgsql
_48
as $$
_48
declare
_48
claims jsonb;
_48
is_admin boolean;
_48
begin
_48
-- Check if the user is marked as admin in the profiles table
_48
select is_admin into is_admin from profiles where user_id = (event->>'user_id')::uuid;
_48
_48
-- Proceed only if the user is an admin
_48
if is_admin then
_48
claims := event->'claims';
_48
_48
-- Check if 'app_metadata' exists in claims
_48
if jsonb_typeof(claims->'app_metadata') is null then
_48
-- If 'app_metadata' does not exist, create an empty object
_48
claims := jsonb_set(claims, '{app_metadata}', '{}');
_48
end if;
_48
_48
-- Set a claim of 'admin'
_48
claims := jsonb_set(claims, '{app_metadata, admin}', 'true');
_48
_48
-- Update the 'claims' object in the original event
_48
event := jsonb_set(event, '{claims}', claims);
_48
end if;
_48
_48
-- Return the modified or original event
_48
return event;
_48
end;
_48
$$;
_48
_48
grant execute
_48
on function public.custom_access_token_hook
_48
to supabase_auth_admin;
_48
_48
revoke execute
_48
on function public.custom_access_token_hook
_48
from authenticated, anon, public;
_48
_48
grant all
_48
on table public.profiles
_48
to supabase_auth_admin;
_48
_48
revoke all
_48
on table public.profiles
_48
from authenticated, anon, public;

Local Development

Unlike the hosted platform, the local development dashboard does not have a user interface for linking a Hook. Instead, edit config.toml to link an Auth Hook and run it with your local setup. Modify the auth.hook.<hook_name> field and set uri to a value of pg-functions://postgres/<schema>/<function_name>

For instance, the config.toml for a Custom Access Token Hook which uses the function public.custom_access_token_hook:


_10
...
_10
_10
[auth.hook.custom_access_token]
_10
enabled = true
_10
uri = "pg-functions://postgres/public/custom_access_token_hook"
_10
_10
...

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.

Debugging Your Hook

Test the Postgres Function in isolation before enabling the hook by connecting to your database with psql and running select <your_function_name>. Where necessary, use RAISE statements to print information to the terminal.