Supabase enrolls the factor, but enforcing it is your job. After password login the session is at aal1 (one factor). After the TOTP code it's aal2.
In the app, after login:
const { data } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (data.nextLevel === 'aal2' && data.currentLevel !== 'aal2') {
// show the code screen, then mfa.challenge + mfa.verify
}That's UX only; users can still navigate away, as you found. The real lock is in the database. A restrictive policy on sensitive tables:
create policy "require mfa when enrolled" on invoices
as restrictive for all to authenticated
using (
array[auth.jwt()->>'aal'] <@ (
select case when count(*) > 0 then array['aal2'] else array['aal1','aal2'] end
from auth.mfa_factors where user_id = auth.uid() and status = 'verified'
)
);Now a user with a verified factor gets no data until they've entered the code, whatever page they go to. Also check it in your middleware so pages redirect nicely.