All blocks
Foundation
MFA enforcement
mfa_satisfied() for restrictive policies: true when the caller has no verified factor, or verified one in this session (aal2).
npx better-supabase sql add mfaimport { defineConfig } from "better-supabase/config";
export default defineConfig({
sql: {
modules: {
// Modes: "managed". The default is "managed".
mfa: { mode: "managed" },
},
},
});npx better-supabase sql add mfa writes these files into your declarative schema with the default layout. See SQL modules for the migration step and the layout options.
supabase/schemas/900_better_supabase_15_mfa.sqlschema47 linessupabase/better-supabase-data/900_better_supabase_15_mfa.sqldata12 lines
supabase/schemas/900_better_supabase_15_mfa.sql
-- better-supabase module: mfa
-- @bs-module mfa@1 managed
-- mfa_satisfied() for restrictive policies: true when the caller has no verified factor, or verified one in this session (aal2).
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
create schema if not exists better_supabase;
grant usage on schema better_supabase to anon, authenticated, service_role;
-- authenticated can't read auth.mfa_factors, so the check runs as the owner.
create or replace function better_supabase.mfa_satisfied()
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
select coalesce(auth.jwt() ->> 'aal', 'aal1') = 'aal2'
or not exists (
select 1
from auth.mfa_factors f
where f.user_id = auth.uid()
and f.status = 'verified'
)
$$;
revoke execute on function better_supabase.mfa_satisfied() from public, anon;
grant execute on function better_supabase.mfa_satisfied() to authenticated;
-- Restrictive, so it applies on top of the table's other policies:
-- create policy mfa_required on public.invoices as restrictive
-- for all to authenticated
-- using ((select better_supabase.mfa_satisfied()))
-- with check ((select better_supabase.mfa_satisfied()));
create schema if not exists better_supabase;
create table if not exists better_supabase.modules (
name text primary key,
version integer not null,
mode text not null,
installed_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table better_supabase.modules enable row level security;
revoke all on better_supabase.modules from anon, authenticated;
grant select on better_supabase.modules to service_role;
supabase/better-supabase-data/900_better_supabase_15_mfa.sql
-- better-supabase module: mfa
-- @bs-module-data mfa
-- Rows and settings a schema diff doesn't capture. Run `better-supabase sql data`
-- after the schema migration to put them in a migration.
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
insert into better_supabase.modules (name, version, mode)
values ('mfa', 1, 'managed')
on conflict (name) do update
set version = excluded.version, mode = excluded.mode, updated_at = now();
This block is SQL only: call its functions from your policies, RPCs and the other modules.
This module writes no outbox events.