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:
Hook | Available on Plan |
---|---|
Custom Access Token | Free, Pro |
MFA Verification Attempt | Teams and Enterprise |
Password Verification Attempt | Teams 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.
If you're using the Supabase SQL Editor, there's an issue when using the ?
(Does the string
exist as a top-level key within the JSON value?) operator. Use a direct connection to the
database if you need to use it when defining a function.
Here is an example hook signature:
_11create or replace function public.custom_access_token_hook(event jsonb)_11returns jsonb_11language plpgsql_11as $$_11declare_11 -- Insert variables here_11begin_11 -- Insert logic here_11 return event;_11end;_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:
_10grant execute_10 on function public.custom_access_token_hook_10 to supabase_auth_admin;
You also need to grant usage to supabase_auth_admin
:
_10grant 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.
_10revoke 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. Usereject
to deny the verification attempt and log the user out of all active sessions. Usecontinue
to use the default Supabase Auth behavior.message
The message to show the user if the decision wasreject
.
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.
_10create 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:
_66create function public.hook_mfa_verification_attempt(event jsonb)_66 returns jsonb_66 language plpgsql_66as $$_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_66grant execute_66 on function public.hook_mfa_verification_attempt_66 to supabase_auth_admin;_66_66grant all_66 on table public.mfa_failed_verification_attempts_66 to supabase_auth_admin;_66_66revoke execute_66 on function public.hook_mfa_verification_attempt_66 from authenticated, anon, public;_66_66revoke 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 theauth.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. Usereject
to completely reject the verification attempt and log the user out of all active sessions. Usecontinue
to use the default Supabase Auth behavior.message
The message to show the user if the decision wasreject
.should_logout_user
Whether to logout a user if areject
decision is issued. Has no effect when acontinue
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.
_10create 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:
_62create function public.hook_password_verification_attempt(event jsonb)_62returns jsonb_62language plpgsql_62as $$_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_62grant execute_62 on function public.hook_password_verification_attempt_62 to supabase_auth_admin;_62_62grant all_62 on table public.password_failed_verification_attempts_62 to supabase_auth_admin;_62_62revoke execute_62 on function public.hook_password_verification_attempt_62 from authenticated, anon, public;_62_62revoke 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 theauth.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:
_10create 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:
_48create or replace function public.custom_access_token_hook(event jsonb)_48returns jsonb_48language plpgsql_48as $$_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_48grant execute_48 on function public.custom_access_token_hook_48 to supabase_auth_admin;_48_48revoke execute_48 on function public.custom_access_token_hook_48 from authenticated, anon, public;_48_48grant all_48 on table public.profiles_48 to supabase_auth_admin;_48_48revoke 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]_10enabled = true_10uri = "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.