MFA Verification Hook
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 send a payload containing these fields to your hook:
json
1{2 "factor_id": "6eab6a69-7766-48bf-95d8-bd8f606894db",3 "user_id": "3919cb6e-4215-4478-a960-6d3454326cec",4 "valid": true5}Outputs
Return this if your hook processed the input without errors.
json
1{2 "decision": "reject",3 "message": "You have exceeded maximum number of MFA attempts."4}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.
sql
1create table public.mfa_failed_verification_attempts (2 user_id uuid not null,3 factor_id uuid not null,4 last_failed_at timestamp not null default now(),5 primary key (user_id, factor_id)6);Create a hook to read and write information to this table. For example:
sql
1create function public.hook_mfa_verification_attempt(event jsonb)2 returns jsonb3 language plpgsql4as $$5 declare6 last_failed_at timestamp;7 begin8 if event->'valid' is true then9 -- code is valid, accept it10 return jsonb_build_object('decision', 'continue');11 end if;1213 select last_failed_at into last_failed_at14 from public.mfa_failed_verification_attempts15 where16 user_id = event->'user_id'17 and18 factor_id = event->'factor_id';1920 if last_failed_at is not null and now() - last_failed_at < interval '2 seconds' then21 -- last attempt was done too quickly22 return jsonb_build_object(23 'error', jsonb_build_object(24 'http_code', 429,25 'message', 'Please wait a moment before trying again.'26 )27 );28 end if;2930 -- record this failed attempt31 insert into public.mfa_failed_verification_attempts32 (33 user_id,34 factor_id,35 last_failed_at36 )37 values38 (39 event->'user_id',40 event->'factor_id',41 now()42 )43 on conflict do update44 set last_failed_at = now();4546 -- finally let Supabase Auth do the default behavior for a failed attempt47 return jsonb_build_object('decision', 'continue');48 end;49$$;5051-- Assign appropriate permissions and revoke access52grant all53 on table public.mfa_failed_verification_attempts54 to supabase_auth_admin;5556revoke all57 on table public.mfa_failed_verification_attempts58 from authenticated, anon, public;